@ -12,12 +12,14 @@ import com.tailscale.ipn.R
import com.tailscale.ipn.UninitializedApp.Companion.notificationManager
import com.tailscale.ipn.UninitializedApp.Companion.notificationManager
import com.tailscale.ipn.ui.model.Health
import com.tailscale.ipn.ui.model.Health
import com.tailscale.ipn.ui.model.Health.UnhealthyState
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.ui.util.set
import com.tailscale.ipn.util.TSLog
import com.tailscale.ipn.util.TSLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
@ -25,6 +27,7 @@ import kotlinx.coroutines.launch
@OptIn ( FlowPreview :: class )
@OptIn ( FlowPreview :: class )
class HealthNotifier (
class HealthNotifier (
healthStateFlow : StateFlow < Health . State ? > ,
healthStateFlow : StateFlow < Health . State ? > ,
ipnStateFlow : StateFlow < Ipn . State > ,
scope : CoroutineScope ,
scope : CoroutineScope ,
) {
) {
companion object {
companion object {
@ -45,8 +48,18 @@ class HealthNotifier(
scope . launch {
scope . launch {
healthStateFlow
healthStateFlow
. distinctUntilChanged { old , new -> old ?. Warnings ?. count ( ) == new ?. Warnings ?. count ( ) }
. distinctUntilChanged { old , new -> old ?. Warnings ?. count ( ) == new ?. Warnings ?. count ( ) }
. combine ( ipnStateFlow , :: Pair )
. debounce ( 5000 )
. debounce ( 5000 )
. collect { health ->
. 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()} " )
TSLog . d ( TAG , " Health updated: ${health?.Warnings?.keys?.sorted()} " )
health ?. Warnings ?. let {
health ?. Warnings ?. let {
notifyHealthUpdated ( it . values . mapNotNull { it } . toTypedArray ( ) )
notifyHealthUpdated ( it . values . mapNotNull { it } . toTypedArray ( ) )
@ -54,6 +67,7 @@ class HealthNotifier(
}
}
}
}
}
}
}
val currentWarnings : StateFlow < Set < UnhealthyState > > = MutableStateFlow ( setOf ( ) )
val currentWarnings : StateFlow < Set < UnhealthyState > > = MutableStateFlow ( setOf ( ) )
val currentIcon : StateFlow < Int ? > = MutableStateFlow ( null )
val currentIcon : StateFlow < Int ? > = MutableStateFlow ( null )
@ -65,8 +79,11 @@ class HealthNotifier(
val removedByNewDependency : MutableSet < UnhealthyState > = mutableSetOf ( )
val removedByNewDependency : MutableSet < UnhealthyState > = mutableSetOf ( )
val isWarmingUp = warnings . any { it . WarnableCode == " warming-up " }
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 ) {
fun dropDependenciesForAddedWarning ( w : UnhealthyState ) {
for ( warning in warningsBeforeAdd ) {
for ( warning in warningsBeforeAdd ) {
warning . DependsOn ?. let {
warning . DependsOn ?. let {
@ -112,6 +129,14 @@ class HealthNotifier(
this . updateIcon ( )
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 ( ) {
private fun updateIcon ( ) {
if ( currentWarnings . value . isEmpty ( ) ) {
if ( currentWarnings . value . isEmpty ( ) ) {
this . currentIcon . set ( null )
this . currentIcon . set ( null )
@ -145,6 +170,16 @@ class HealthNotifier(
notificationManager . notify ( code . hashCode ( ) , notification )
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 > ) {
private fun removeNotifications ( warnings : Set < UnhealthyState > ) {
TSLog . d ( TAG , " Removing notifications for $warnings " )
TSLog . d ( TAG , " Removing notifications for $warnings " )
for ( warning in warnings ) {
for ( warning in warnings ) {