Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Settings: Update VPN settings item #5394

Merged
merged 20 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,8 @@ interface NetworkProtectionState {
CONNECTED,
CONNECTING,
DISCONNECTED,
;

fun isConnected(): Boolean = this == CONNECTED
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.networkprotection.impl.subscription
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus
import com.duckduckgo.settings.api.NewSettingsFeature
import com.duckduckgo.subscriptions.api.Product.NetP
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.Subscriptions
Expand All @@ -37,6 +38,8 @@ interface NetpSubscriptionManager {
EXPIRED,
SIGNED_OUT,
INACTIVE,
WAITING,
INELIGIBLE,
}
}

Expand All @@ -52,6 +55,7 @@ fun VpnStatus.isExpired(): Boolean {
class RealNetpSubscriptionManager @Inject constructor(
private val subscriptions: Subscriptions,
private val dispatcherProvider: DispatcherProvider,
private val newSettingsFeature: NewSettingsFeature,
) : NetpSubscriptionManager {

override suspend fun getVpnStatus(): VpnStatus {
Expand All @@ -71,15 +75,29 @@ class RealNetpSubscriptionManager @Inject constructor(
private fun hasValidEntitlementFlow(): Flow<Boolean> = subscriptions.getEntitlementStatus().map { it.contains(NetP) }

private suspend fun getVpnStatusInternal(hasValidEntitlement: Boolean): VpnStatus {
val subscriptionState = subscriptions.getSubscriptionStatus()
return when (subscriptionState) {
SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
else -> {
if (hasValidEntitlement) {
VpnStatus.ACTIVE
} else {
VpnStatus.INACTIVE
return if (newSettingsFeature.self().isEnabled()) {
when {
!hasValidEntitlement -> VpnStatus.INELIGIBLE
else -> {
when (subscriptions.getSubscriptionStatus()) {
SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
SubscriptionStatus.AUTO_RENEWABLE, SubscriptionStatus.NOT_AUTO_RENEWABLE, SubscriptionStatus.GRACE_PERIOD -> VpnStatus.ACTIVE
SubscriptionStatus.WAITING -> VpnStatus.WAITING
}
}
}
} else {
val subscriptionState = subscriptions.getSubscriptionStatus()
when (subscriptionState) {
SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
else -> {
if (hasValidEntitlement) {
VpnStatus.ACTIVE
} else {
VpnStatus.INACTIVE
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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.impl.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.findViewTreeLifecycleOwner
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.mobile.android.R as CommonR
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.networkprotection.impl.databinding.LegacyViewSettingsNetpBinding
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command.OpenNetPScreen
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Factory
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Hidden
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Pending
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.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 LegacyProSettingNetPView @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: LegacyViewSettingsNetpBinding by viewBinding()

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

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

findViewTreeLifecycleOwner()?.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.setLeadingIconResource(CommonR.drawable.ic_check_grey_round_16)
}
is ShowState -> {
this.show()
this.setLeadingIconResource(networkProtectionEntryState.icon)
}
}
}
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
findViewTreeLifecycleOwner()?.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,163 @@
/*
* 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.impl.subscription.settings

import android.annotation.SuppressLint
import androidx.annotation.DrawableRes
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.utils.DispatcherProvider
import com.duckduckgo.mobile.android.R as CommonR
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState
import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState
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.impl.R
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command.OpenNetPScreen
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Hidden
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Pending
import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.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.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import logcat.logcat

@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
class LegacyProSettingNetPViewModel(
private val networkProtectionAccessState: NetworkProtectionAccessState,
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(
@DrawableRes val icon: Int,
@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 onStart(owner: LifecycleOwner) {
super.onStart(owner)

viewModelScope.launch {
combine(networkProtectionAccessState.getStateFlow(), networkProtectionState.getConnectionStateFlow()) { accessState, connectionState ->
_viewState.emit(
viewState.value.copy(
networkProtectionEntryState = getNetworkProtectionEntryState(accessState, connectionState),
),
)
}.flowOn(dispatcherProvider.main()).launchIn(viewModelScope)
}
}

fun onNetPSettingClicked() {
viewModelScope.launch {
val screen = networkProtectionAccessState.getScreenForCurrentState()
screen?.let {
command.send(OpenNetPScreen(screen))
pixel.fire(NETP_SETTINGS_PRESSED)
} ?: logcat { "Get screen for current NetP state is null" }
}
}

private suspend fun getNetworkProtectionEntryState(
accessState: NetPAccessState,
networkProtectionConnectionState: ConnectionState,
): NetPEntryState {
return when (accessState) {
is NetPAccessState.UnLocked -> {
if (networkProtectionState.isOnboarded()) {
val subtitle = when (networkProtectionConnectionState) {
CONNECTED -> R.string.netpSubscriptionSettingsConnected
CONNECTING -> R.string.netpSubscriptionSettingsConnecting
else -> R.string.netpSubscriptionSettingsDisconnected
}

val netPItemIcon = if (networkProtectionConnectionState != DISCONNECTED) {
CommonR.drawable.ic_check_green_round_16
} else {
CommonR.drawable.ic_exclamation_yellow_16
}

ShowState(
icon = netPItemIcon,
subtitle = subtitle,
)
} else {
Pending
}
}

NetPAccessState.Locked -> Hidden
}
}

@Suppress("UNCHECKED_CAST")
class Factory @Inject constructor(
private val networkProtectionAccessState: NetworkProtectionAccessState,
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(LegacyProSettingNetPViewModel::class.java) -> LegacyProSettingNetPViewModel(
networkProtectionAccessState,
networkProtectionState,
dispatcherProvider,
pixel,
)

else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
}
}
Loading
Loading