Skip to content

Commit fda3820

Browse files
authored
HealthNotifier: prevent and drop all warnings in the Stopped state (#575)
Updates tailscale/tailscale#12960 When the client is Stopped after running, a false positive DERP warnings was getting presented. This was not happening on Apple platforms because we never leave the client in a Stopped state, the extension instantly terminates. Since that's not the case on Android, this PR ensures that: - we do not present any warnings when the client is Stopped (nothing should be broken when nothing is running) - if we enter the Stopped state, any pre-existing warnings generated while the client was running are dropped Signed-off-by: Andrea Gottardo <[email protected]>
1 parent 61c7c3c commit fda3820

File tree

2 files changed

+42
-7
lines changed

2 files changed

+42
-7
lines changed

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
158158
Request.setApp(app)
159159
Notifier.setApp(app)
160160
Notifier.start(applicationScope)
161-
healthNotifier = HealthNotifier(Notifier.health, applicationScope)
161+
healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope)
162162
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
163163
NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns)
164164
initViewModels()

android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@ import com.tailscale.ipn.R
1212
import com.tailscale.ipn.UninitializedApp.Companion.notificationManager
1313
import com.tailscale.ipn.ui.model.Health
1414
import com.tailscale.ipn.ui.model.Health.UnhealthyState
15+
import com.tailscale.ipn.ui.model.Ipn
1516
import com.tailscale.ipn.ui.util.set
1617
import com.tailscale.ipn.util.TSLog
1718
import kotlinx.coroutines.CoroutineScope
1819
import kotlinx.coroutines.FlowPreview
1920
import kotlinx.coroutines.flow.MutableStateFlow
2021
import kotlinx.coroutines.flow.StateFlow
22+
import kotlinx.coroutines.flow.combine
2123
import kotlinx.coroutines.flow.debounce
2224
import kotlinx.coroutines.flow.distinctUntilChanged
2325
import kotlinx.coroutines.launch
2426

2527
@OptIn(FlowPreview::class)
2628
class HealthNotifier(
2729
healthStateFlow: StateFlow<Health.State?>,
30+
ipnStateFlow: StateFlow<Ipn.State>,
2831
scope: CoroutineScope,
2932
) {
3033
companion object {
@@ -45,11 +48,22 @@ class HealthNotifier(
4548
scope.launch {
4649
healthStateFlow
4750
.distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() }
51+
.combine(ipnStateFlow, ::Pair)
4852
.debounce(5000)
49-
.collect { health ->
50-
TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}")
51-
health?.Warnings?.let {
52-
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray())
53+
.collect { pair ->
54+
val health = pair.first
55+
val ipnState = pair.second
56+
// When the client is Stopped, no warnings should get added, and any warnings added
57+
// previously should be removed.
58+
if (ipnState == Ipn.State.Stopped) {
59+
TSLog.d(TAG, "Ignoring and dropping all pre-existing health messages in the Stopped state")
60+
dropAllWarnings()
61+
return@collect
62+
} else {
63+
TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}")
64+
health?.Warnings?.let {
65+
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray())
66+
}
5367
}
5468
}
5569
}
@@ -65,8 +79,11 @@ class HealthNotifier(
6579
val removedByNewDependency: MutableSet<UnhealthyState> = mutableSetOf()
6680
val isWarmingUp = warnings.any { it.WarnableCode == "warming-up" }
6781

68-
/// Checks if there is any warning in `warningsBeforeAdd` that needs to be removed because the new warning `w`
69-
/// is listed as a dependency of a warning already in `warningsBeforeAdd`, and removes it.
82+
/**
83+
* dropDependenciesForAddedWarning checks if there is any warning in `warningsBeforeAdd` that
84+
* needs to be removed because the new warning `w` is listed as a dependency of a warning
85+
* already in `warningsBeforeAdd`, and removes it.
86+
*/
7087
fun dropDependenciesForAddedWarning(w: UnhealthyState) {
7188
for (warning in warningsBeforeAdd) {
7289
warning.DependsOn?.let {
@@ -112,6 +129,14 @@ class HealthNotifier(
112129
this.updateIcon()
113130
}
114131

132+
/**
133+
* Sets the icon displayed to represent the overall health state.
134+
*
135+
* - If there are any high severity warnings, or warnings that affect internet connectivity,
136+
* a warning icon is displayed.
137+
* - If there are any other kind of warnings, an info icon is displayed.
138+
* - If there are no warnings at all, no icon is set.
139+
*/
115140
private fun updateIcon() {
116141
if (currentWarnings.value.isEmpty()) {
117142
this.currentIcon.set(null)
@@ -145,6 +170,16 @@ class HealthNotifier(
145170
notificationManager.notify(code.hashCode(), notification)
146171
}
147172

173+
/**
174+
* Removes all warnings currently displayed, including any system notifications, and
175+
* updates the icon (causing it to be set to null since the set of warnings is empty).
176+
*/
177+
private fun dropAllWarnings() {
178+
removeNotifications(this.currentWarnings.value)
179+
this.currentWarnings.set(emptySet())
180+
this.updateIcon()
181+
}
182+
148183
private fun removeNotifications(warnings: Set<UnhealthyState>) {
149184
TSLog.d(TAG, "Removing notifications for $warnings")
150185
for (warning in warnings) {

0 commit comments

Comments
 (0)