android: add NotificationsManager

Andrea Gottardo 2 months ago
parent f96e9b923f
commit 3db8196a7f

@ -48,6 +48,7 @@ import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.localapi.Request
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.NotificationsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -97,6 +98,7 @@ class App : Application(), libtailscale.AppContext {
var vpnReady = false
private lateinit var connectivityManager: ConnectivityManager
private lateinit var app: libtailscale.Application
private lateinit var notificationsManager: NotificationsManager
override fun getPlatformDNSConfig(): String = dns.dnsConfigAsString
@ -124,6 +126,7 @@ class App : Application(), libtailscale.AppContext {
applicationScope.launch {
Notifier.tileReady.collect { isTileReady -> setTileReady(isTileReady) }
}
notificationsManager = NotificationsManager(Notifier)
}
override fun onTerminate() {

@ -3,10 +3,12 @@
package com.tailscale.ipn
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.RestrictionsManager
import android.content.pm.PackageManager
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
@ -15,6 +17,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@ -108,9 +111,7 @@ class MainActivity : ComponentActivity() {
MullvadExitNodePicker(
it.arguments!!.getString("countryCode")!!, exitNodePickerNav)
}
composable("runExitNode") {
RunExitNodeView(exitNodePickerNav)
}
composable("runExitNode") { RunExitNodeView(exitNodePickerNav) }
}
composable(
"peerDetails/{nodeId}",
@ -166,6 +167,7 @@ class MainActivity : ComponentActivity() {
// (jonathan) TODO: Requesting VPN permissions onStart is a bit aggressive. This should
// be done when the user initiall starts the VPN
requestVpnPermission()
requestNotificationsPermission()
}
override fun onStop() {
@ -191,6 +193,16 @@ class MainActivity : ComponentActivity() {
Log.i("VPN", "VPN permission granted")
}
}
private fun requestNotificationsPermission() {
if (ActivityCompat.checkSelfPermission(
App.getApplication().applicationContext, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
Log.d("PersistentNotifications", "Missing permission to deliver notifications, requesting")
ActivityCompat.requestPermissions(
this, listOf<String>(Manifest.permission.POST_NOTIFICATIONS).toTypedArray(), 0)
}
}
}
class VpnPermissionContract : ActivityResultContract<Unit, Boolean>() {

@ -0,0 +1,138 @@
package com.tailscale.ipn.ui.util
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context.NOTIFICATION_SERVICE
import android.content.pm.PackageManager
import android.util.Log
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getString
import com.tailscale.ipn.App
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.viewModel.AdvertisedRoutesHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
// NotificationsManager observes the state of the tunnel and the preferences
// to deliver or cancel system-wide notifications.
class NotificationsManager(notifier: Notifier) {
private val RUN_AS_EXIT_NODE_NOTIFICATION_CHANNEL = "RunAsExitNode"
private val RUN_AS_EXIT_NODE_NOTIFICATION_ID = 201
private val NEEDS_APPROVAL_NOTIFICATION_CHANNEL = "NeedsMachineAuth"
private val NEEDS_APPROVAL_NOTIFICATION_ID = 202
private val notificationManager: NotificationManager =
App.getApplication().getSystemService(NOTIFICATION_SERVICE) as NotificationManager
init {
CoroutineScope(Dispatchers.Default).launch {
notifier.prefs
.combine(notifier.state) { prefs, state -> Pair(prefs, state) }
.collect { (prefs, state) ->
prefs?.let { notifyWithPrefs(it) }
notifyWithState(state)
}
}
}
private fun notifyWithPrefs(prefs: Ipn.Prefs) {
notifyRunningAsExitNode(AdvertisedRoutesHelper.exitNodeOnFromPrefs(prefs))
}
private fun notifyWithState(state: Ipn.State) {
notifyNeedsMachineAuth(state == Ipn.State.NeedsMachineAuth)
}
private fun notifyRunningAsExitNode(isOn: Boolean) {
ensureChannelRegistered(
id = RUN_AS_EXIT_NODE_NOTIFICATION_CHANNEL,
name = R.string.running_as_exit_node,
description =
R.string
.other_devices_can_access_the_internet_using_the_ip_address_of_this_device_you_can_turn_this_off_in_the_tailscale_app)
if (isOn) {
deliverNotification(
id = RUN_AS_EXIT_NODE_NOTIFICATION_ID,
channel = RUN_AS_EXIT_NODE_NOTIFICATION_CHANNEL,
title = R.string.running_as_exit_node,
text =
R.string
.other_devices_can_access_the_internet_using_the_ip_address_of_this_device_you_can_turn_this_off_in_the_tailscale_app,
isOngoing = true,
isSilent = true)
} else {
cancelNotifications(RUN_AS_EXIT_NODE_NOTIFICATION_ID)
}
}
private fun notifyNeedsMachineAuth(needsAuth: Boolean) {
ensureChannelRegistered(
id = NEEDS_APPROVAL_NOTIFICATION_CHANNEL,
name = R.string.awaiting_approval,
description = R.string.notifications_to_inform_the_user_when_device_approval_is_needed)
if (needsAuth) {
deliverNotification(
id = NEEDS_APPROVAL_NOTIFICATION_ID,
channel = NEEDS_APPROVAL_NOTIFICATION_CHANNEL,
title = R.string.awaiting_approval,
text =
R.string
.this_device_must_be_approved_in_the_tailscale_admin_console_before_it_can_connect)
} else {
cancelNotifications(id = NEEDS_APPROVAL_NOTIFICATION_ID)
}
}
private fun deliverNotification(
id: Int,
channel: String,
@StringRes title: Int,
@StringRes text: Int,
isOngoing: Boolean = false,
isSilent: Boolean = false
) {
val builder =
NotificationCompat.Builder(App.getApplication().applicationContext, channel)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(App.getApplication().applicationContext, title))
.setOngoing(isOngoing)
.setContentText(getString(App.getApplication().applicationContext, text))
.setSilent(isSilent)
val notification = builder.build()
if (ActivityCompat.checkSelfPermission(
App.getApplication().applicationContext, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
Log.d("PersistentNotifications", "Missing permission to deliver notifications")
}
notificationManager.notify(id, notification)
}
private fun cancelNotifications(id: Int) {
notificationManager.cancel(id)
}
private fun ensureChannelRegistered(
id: String,
@StringRes name: Int,
@StringRes description: Int
) {
val nameText = getString(App.getApplication().applicationContext, name)
val descriptionText = getString(App.getApplication().applicationContext, description)
val channel =
NotificationChannel(id, nameText, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.description = descriptionText
}
notificationManager.createNotificationChannel(channel)
}
}

@ -96,7 +96,7 @@
<string name="run_exit_node_caution">Caution: Running an exit node will severely impact battery life. On a metered data plan, significant cellular data charges may also apply. Always disable this feature when no longer needed.</string>
<string name="stop_running_as_exit_node">Stop Running as Exit Node</string>
<string name="start_running_as_exit_node">Start Running as Exit Node</string>
<string name="running_as_exit_node">Now Running as Exit Node</string>
<string name="running_as_exit_node">Running as Exit Node</string>
<string name="run_exit_node_explainer_running">Other devices in your tailnet can now route their Internet traffic through this Android device. Make sure to approve this exit node in the admin console in order for other devices to see it.</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
@ -115,5 +115,10 @@
<string name="tailnet_lock_key_explainer">Used to authorize changes to the Tailnet lock configuration.</string>
<string name="bug_report_id">Bug Report ID</string>
<string name="learn_more">Learn more…</string>
<string name="shows_a_notification_when_the_device_is_being_used_as_an_exit_node">Shows a notification when the device is being used as an exit node.</string>
<string name="other_devices_can_access_the_internet_using_the_ip_address_of_this_device_you_can_turn_this_off_in_the_tailscale_app">Other devices can access the Internet using this device. You can disable this in the Tailscale app.</string>
<string name="awaiting_approval">Awaiting Approval</string>
<string name="notifications_to_inform_the_user_when_device_approval_is_needed">Notifications to inform the user when device approval is needed.</string>
<string name="this_device_must_be_approved_in_the_tailscale_admin_console_before_it_can_connect">This device must be approved in the Tailscale admin console before it can connect.</string>
</resources>

Loading…
Cancel
Save