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

HealthNotifier: prevent and drop all warnings in the Stopped state #575

Merged
merged 1 commit into from
Nov 26, 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
2 changes: 1 addition & 1 deletion android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
Request.setApp(app)
Notifier.setApp(app)
Notifier.start(applicationScope)
healthNotifier = HealthNotifier(Notifier.health, applicationScope)
healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns)
initViewModels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ import com.tailscale.ipn.R
import com.tailscale.ipn.UninitializedApp.Companion.notificationManager
import com.tailscale.ipn.ui.model.Health
import com.tailscale.ipn.ui.model.Health.UnhealthyState
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.util.TSLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch

@OptIn(FlowPreview::class)
class HealthNotifier(
healthStateFlow: StateFlow<Health.State?>,
ipnStateFlow: StateFlow<Ipn.State>,
scope: CoroutineScope,
) {
companion object {
Expand All @@ -45,11 +48,22 @@ class HealthNotifier(
scope.launch {
healthStateFlow
.distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() }
.combine(ipnStateFlow, ::Pair)
.debounce(5000)
.collect { health ->
TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}")
health?.Warnings?.let {
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray())
.collect { pair ->
val health = pair.first
val ipnState = pair.second
// When the client is Stopped, no warnings should get added, and any warnings added
// previously should be removed.
if (ipnState == Ipn.State.Stopped) {
TSLog.d(TAG, "Ignoring and dropping all pre-existing health messages in the Stopped state")
dropAllWarnings()
return@collect
} else {
TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}")
health?.Warnings?.let {
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray())
}
}
}
}
Expand All @@ -65,8 +79,11 @@ class HealthNotifier(
val removedByNewDependency: MutableSet<UnhealthyState> = mutableSetOf()
val isWarmingUp = warnings.any { it.WarnableCode == "warming-up" }

/// Checks if there is any warning in `warningsBeforeAdd` that needs to be removed because the new warning `w`
/// is listed as a dependency of a warning already in `warningsBeforeAdd`, and removes it.
/**
* dropDependenciesForAddedWarning checks if there is any warning in `warningsBeforeAdd` that
* needs to be removed because the new warning `w` is listed as a dependency of a warning
* already in `warningsBeforeAdd`, and removes it.
*/
fun dropDependenciesForAddedWarning(w: UnhealthyState) {
for (warning in warningsBeforeAdd) {
warning.DependsOn?.let {
Expand Down Expand Up @@ -112,6 +129,14 @@ class HealthNotifier(
this.updateIcon()
}

/**
* Sets the icon displayed to represent the overall health state.
*
* - If there are any high severity warnings, or warnings that affect internet connectivity,
* a warning icon is displayed.
* - If there are any other kind of warnings, an info icon is displayed.
* - If there are no warnings at all, no icon is set.
*/
private fun updateIcon() {
if (currentWarnings.value.isEmpty()) {
this.currentIcon.set(null)
Expand Down Expand Up @@ -145,6 +170,16 @@ class HealthNotifier(
notificationManager.notify(code.hashCode(), notification)
}

/**
* Removes all warnings currently displayed, including any system notifications, and
* updates the icon (causing it to be set to null since the set of warnings is empty).
*/
private fun dropAllWarnings() {
removeNotifications(this.currentWarnings.value)
this.currentWarnings.set(emptySet())
this.updateIcon()
}

private fun removeNotifications(warnings: Set<UnhealthyState>) {
TSLog.d(TAG, "Removing notifications for $warnings")
for (warning in warnings) {
Expand Down