-
Notifications
You must be signed in to change notification settings - Fork 454
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui: deliver health notifications to user
Updates tailscale/tailscale#4136 This PR adds support for notifying the user when health warnings are sent down coming from LocalAPI. We remove duplicates and debounce updates; then deliver a notification for each health warning are they are sent down. Just like on macOS, notifications are removed when a Warnable becomes healthy again. Notifications are delivered on a separate notification channel, so they can be disabled if needed. Signed-off-by: Andrea Gottardo <[email protected]>
- Loading branch information
Showing
7 changed files
with
178 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
android/src/main/java/com/tailscale/ipn/ui/model/Health.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright (c) Tailscale Inc & AUTHORS | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
|
||
package com.tailscale.ipn.ui.model | ||
|
||
import kotlinx.serialization.Serializable | ||
|
||
class Health { | ||
@Serializable | ||
data class State( | ||
// WarnableCode -> UnhealthyState or null | ||
var Warnings: Map<String, UnhealthyState?>? = null, | ||
) | ||
|
||
@Serializable | ||
data class UnhealthyState( | ||
var WarnableCode: String, | ||
var Severity: Severity, | ||
var Title: String, | ||
var Text: String, | ||
var BrokenSince: String? = null, | ||
var Args: Map<String, String>? = null, | ||
var DependsOn: List<String>? = null, // an array of WarnableCodes this depends on | ||
) { | ||
fun hiddenByDependencies(currentWarnableCodes: Set<String>): Boolean { | ||
return this.DependsOn?.let { | ||
it.any { depWarnableCode -> currentWarnableCodes.contains(depWarnableCode) } | ||
} == true | ||
} | ||
} | ||
|
||
@Serializable | ||
enum class Severity { | ||
high, | ||
medium, | ||
low | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// Copyright (c) Tailscale Inc & AUTHORS | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
|
||
package com.tailscale.ipn.ui.notifier | ||
|
||
import android.Manifest | ||
import android.content.pm.PackageManager | ||
import android.util.Log | ||
import androidx.core.app.ActivityCompat | ||
import androidx.core.app.NotificationCompat | ||
import com.tailscale.ipn.App | ||
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 kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.FlowPreview | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.debounce | ||
import kotlinx.coroutines.flow.distinctUntilChanged | ||
import kotlinx.coroutines.launch | ||
|
||
@OptIn(FlowPreview::class) | ||
class HealthNotifier( | ||
healthStateFlow: StateFlow<Health.State?>, | ||
scope: CoroutineScope, | ||
) { | ||
companion object { | ||
const val HEALTH_CHANNEL_ID = "tailscale-health" | ||
} | ||
|
||
private val TAG = "Health" | ||
private val ignoredWarnableCodes: Set<String> = | ||
setOf( | ||
// Ignored on Android because installing unstable takes quite some effort | ||
"is-using-unstable-version", | ||
|
||
// Ignored on Android because we already have a dedicated connected/not connected | ||
// notification | ||
"wantrunning-false") | ||
|
||
init { | ||
scope.launch { | ||
healthStateFlow | ||
.distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() } | ||
.debounce(5000) | ||
.collect { health -> | ||
Log.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") | ||
health?.Warnings?.let { | ||
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private val currentWarnings: MutableSet<String> = mutableSetOf() | ||
|
||
private fun notifyHealthUpdated(warnings: Array<UnhealthyState>) { | ||
val warningsBeforeAdd = currentWarnings | ||
val currentWarnableCodes = warnings.map { it.WarnableCode }.toSet() | ||
|
||
val addedWarnings: MutableSet<String> = mutableSetOf() | ||
for (warning in warnings) { | ||
if (ignoredWarnableCodes.contains(warning.WarnableCode)) { | ||
continue | ||
} | ||
|
||
addedWarnings.add(warning.WarnableCode) | ||
|
||
if (this.currentWarnings.contains(warning.WarnableCode)) { | ||
// Already notified, skip | ||
continue | ||
} else if (warning.hiddenByDependencies(currentWarnableCodes)) { | ||
// Ignore this warning because a dependency is also unhealthy | ||
Log.d(TAG, "Ignoring ${warning.WarnableCode} because of dependency") | ||
continue | ||
} else { | ||
Log.d(TAG, "Adding health warning: ${warning.WarnableCode}") | ||
this.currentWarnings.add(warning.WarnableCode) | ||
this.sendNotification(warning.Title, warning.Text, warning.WarnableCode) | ||
} | ||
} | ||
|
||
val warningsToDrop = warningsBeforeAdd.minus(addedWarnings) | ||
if (warningsToDrop.isNotEmpty()) { | ||
Log.d(TAG, "Dropping health warnings with codes $warningsToDrop") | ||
this.removeNotifications(warningsToDrop) | ||
} | ||
currentWarnings.subtract(warningsToDrop) | ||
} | ||
|
||
private fun sendNotification(title: String, text: String, code: String) { | ||
Log.d(TAG, "Sending notification for $code") | ||
val notification = | ||
NotificationCompat.Builder(App.get().applicationContext, HEALTH_CHANNEL_ID) | ||
.setSmallIcon(R.drawable.ic_notification) | ||
.setContentTitle(title) | ||
.setContentText(text) | ||
.setStyle(NotificationCompat.BigTextStyle().bigText(text)) | ||
.setPriority(NotificationCompat.PRIORITY_HIGH) | ||
.build() | ||
if (ActivityCompat.checkSelfPermission( | ||
App.get().applicationContext, Manifest.permission.POST_NOTIFICATIONS) != | ||
PackageManager.PERMISSION_GRANTED) { | ||
Log.d(TAG, "Notification permission not granted") | ||
return | ||
} | ||
notificationManager.notify(code.hashCode(), notification) | ||
} | ||
|
||
private fun removeNotifications(codes: Set<String>) { | ||
Log.d(TAG, "Removing notifications for $codes") | ||
for (code in codes) { | ||
notificationManager.cancel(code.hashCode()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters