android: hide disconnect action if force enabled (#539)

In notification, don't show 'Disconnect' button if MDM force enable is on.

Fixes tailscale/corp#23764

Signed-off-by: kari-ts <kari@tailscale.com>
pull/549/head
kari-ts 1 month ago committed by GitHub
parent 2e9f6b735e
commit af98b14770
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -36,6 +36,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -141,17 +143,32 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
initViewModels()
applicationScope.launch {
Notifier.state.collect { state ->
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
// If VPN is stopped, show a disconnected notification. If it is running as a foregrround
// service, IPNService will show a connected notification.
if (state == Ipn.State.Stopped) {
notifyStatus(false)
}
val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running
updateConnStatus(ableToStartVPN)
QuickToggleService.setVPNRunning(vpnRunning)
combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled ->
Pair(state, forceEnabled)
}
.collect { (state, hideDisconnectAction) ->
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
// If VPN is stopped, show a disconnected notification. If it is running as a
// foreground
// service, IPNService will show a connected notification.
if (state == Ipn.State.Stopped) {
notifyStatus(vpnRunning = false, hideDisconnectAction = hideDisconnectAction.value)
}
val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running
updateConnStatus(ableToStartVPN)
QuickToggleService.setVPNRunning(vpnRunning)
// Update notification status when VPN is running
if (vpnRunning) {
notifyStatus(vpnRunning = true, hideDisconnectAction = hideDisconnectAction.value)
}
}
}
}
applicationScope.launch {
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
}
}
private fun initViewModels() {
@ -419,8 +436,8 @@ open class UninitializedApp : Application() {
notificationManager.createNotificationChannel(channel)
}
fun notifyStatus(vpnRunning: Boolean) {
notifyStatus(buildStatusNotification(vpnRunning))
fun notifyStatus(vpnRunning: Boolean, hideDisconnectAction: Boolean) {
notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction))
}
fun notifyStatus(notification: Notification) {
@ -438,7 +455,7 @@ open class UninitializedApp : Application() {
notificationManager.notify(STATUS_NOTIFICATION_ID, notification)
}
fun buildStatusNotification(vpnRunning: Boolean): Notification {
fun buildStatusNotification(vpnRunning: Boolean, hideDisconnectAction: Boolean): Notification {
val message = getString(if (vpnRunning) R.string.connected else R.string.not_connected)
val icon = if (vpnRunning) R.drawable.ic_notification else R.drawable.ic_notification_disabled
val action =
@ -460,19 +477,22 @@ open class UninitializedApp : Application() {
PendingIntent.getActivity(
this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
.setSmallIcon(icon)
.setContentTitle("Tailscale")
.setContentText(message)
.setAutoCancel(!vpnRunning)
.setOnlyAlertOnce(!vpnRunning)
.setOngoing(vpnRunning)
.setSilent(true)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build())
.setContentIntent(pendingIntent)
.build()
val builder =
NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
.setSmallIcon(icon)
.setContentTitle(getString(R.string.app_name))
.setContentText(message)
.setAutoCancel(!vpnRunning)
.setOnlyAlertOnce(!vpnRunning)
.setOngoing(vpnRunning)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
if (!vpnRunning || !hideDisconnectAction) {
builder.addAction(
NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build())
}
return builder.build()
}
fun addUserDisallowedPackageName(packageName: String) {

@ -12,6 +12,10 @@ import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.util.TSLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import libtailscale.Libtailscale
import java.util.UUID
@ -19,6 +23,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
private val TAG = "IPNService"
private val randomID: String = UUID.randomUUID().toString()
private lateinit var app: App
val scope = CoroutineScope(Dispatchers.IO)
override fun id(): String {
return randomID
@ -42,7 +47,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
START_NOT_STICKY
}
ACTION_START_VPN -> {
showForegroundNotification()
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
showForegroundNotification(hideDisconnectAction.value)
}
app.setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
@ -51,7 +60,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
// This means we were started by Android due to Always On VPN.
// We show a non-foreground notification because we weren't
// started as a foreground service.
app.notifyStatus(true)
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
app.notifyStatus(true, hideDisconnectAction.value)
}
app.setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
@ -60,7 +73,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
// This means that we were restarted after the service was killed
// (potentially due to OOM).
if (UninitializedApp.get().isAbleToStartVPN()) {
showForegroundNotification()
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
showForegroundNotification(hideDisconnectAction.value)
}
App.get()
Libtailscale.requestVPN(this)
START_STICKY
@ -77,7 +94,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
Libtailscale.serviceDisconnect(this)
}
override fun disconnectVPN(){
override fun disconnectVPN() {
stopSelf()
}
@ -97,11 +114,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
app.getAppScopedViewModel().setVpnPrepared(isPrepared)
}
private fun showForegroundNotification() {
private fun showForegroundNotification(hideDisconnectAction: Boolean) {
try {
startForeground(
UninitializedApp.STATUS_NOTIFICATION_ID,
UninitializedApp.get().buildStatusNotification(true))
UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction))
} catch (e: Exception) {
TSLog.e(TAG, "Failed to start foreground service: $e")
}

Loading…
Cancel
Save