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.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -141,17 +143,32 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
initViewModels() initViewModels()
applicationScope.launch { applicationScope.launch {
Notifier.state.collect { state -> Notifier.state.collect { state ->
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled ->
// If VPN is stopped, show a disconnected notification. If it is running as a foregrround Pair(state, forceEnabled)
// service, IPNService will show a connected notification. }
if (state == Ipn.State.Stopped) { .collect { (state, hideDisconnectAction) ->
notifyStatus(false) val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
} // If VPN is stopped, show a disconnected notification. If it is running as a
val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running // foreground
updateConnStatus(ableToStartVPN) // service, IPNService will show a connected notification.
QuickToggleService.setVPNRunning(vpnRunning) 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() { private fun initViewModels() {
@ -419,8 +436,8 @@ open class UninitializedApp : Application() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
fun notifyStatus(vpnRunning: Boolean) { fun notifyStatus(vpnRunning: Boolean, hideDisconnectAction: Boolean) {
notifyStatus(buildStatusNotification(vpnRunning)) notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction))
} }
fun notifyStatus(notification: Notification) { fun notifyStatus(notification: Notification) {
@ -438,7 +455,7 @@ open class UninitializedApp : Application() {
notificationManager.notify(STATUS_NOTIFICATION_ID, notification) 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 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 icon = if (vpnRunning) R.drawable.ic_notification else R.drawable.ic_notification_disabled
val action = val action =
@ -460,19 +477,22 @@ open class UninitializedApp : Application() {
PendingIntent.getActivity( PendingIntent.getActivity(
this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(this, STATUS_CHANNEL_ID) val builder =
.setSmallIcon(icon) NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
.setContentTitle("Tailscale") .setSmallIcon(icon)
.setContentText(message) .setContentTitle(getString(R.string.app_name))
.setAutoCancel(!vpnRunning) .setContentText(message)
.setOnlyAlertOnce(!vpnRunning) .setAutoCancel(!vpnRunning)
.setOngoing(vpnRunning) .setOnlyAlertOnce(!vpnRunning)
.setSilent(true) .setOngoing(vpnRunning)
.setOngoing(false) .setSilent(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build()) .setContentIntent(pendingIntent)
.setContentIntent(pendingIntent) if (!vpnRunning || !hideDisconnectAction) {
.build() builder.addAction(
NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build())
}
return builder.build()
} }
fun addUserDisallowedPackageName(packageName: String) { 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.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.util.TSLog 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 libtailscale.Libtailscale
import java.util.UUID import java.util.UUID
@ -19,6 +23,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
private val TAG = "IPNService" private val TAG = "IPNService"
private val randomID: String = UUID.randomUUID().toString() private val randomID: String = UUID.randomUUID().toString()
private lateinit var app: App private lateinit var app: App
val scope = CoroutineScope(Dispatchers.IO)
override fun id(): String { override fun id(): String {
return randomID return randomID
@ -42,7 +47,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
START_NOT_STICKY START_NOT_STICKY
} }
ACTION_START_VPN -> { 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) app.setWantRunning(true)
Libtailscale.requestVPN(this) Libtailscale.requestVPN(this)
START_STICKY START_STICKY
@ -51,7 +60,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
// This means we were started by Android due to Always On VPN. // This means we were started by Android due to Always On VPN.
// We show a non-foreground notification because we weren't // We show a non-foreground notification because we weren't
// started as a foreground service. // 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) app.setWantRunning(true)
Libtailscale.requestVPN(this) Libtailscale.requestVPN(this)
START_STICKY START_STICKY
@ -60,7 +73,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
// This means that we were restarted after the service was killed // This means that we were restarted after the service was killed
// (potentially due to OOM). // (potentially due to OOM).
if (UninitializedApp.get().isAbleToStartVPN()) { 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() App.get()
Libtailscale.requestVPN(this) Libtailscale.requestVPN(this)
START_STICKY START_STICKY
@ -77,7 +94,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
Libtailscale.serviceDisconnect(this) Libtailscale.serviceDisconnect(this)
} }
override fun disconnectVPN(){ override fun disconnectVPN() {
stopSelf() stopSelf()
} }
@ -97,11 +114,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
app.getAppScopedViewModel().setVpnPrepared(isPrepared) app.getAppScopedViewModel().setVpnPrepared(isPrepared)
} }
private fun showForegroundNotification() { private fun showForegroundNotification(hideDisconnectAction: Boolean) {
try { try {
startForeground( startForeground(
UninitializedApp.STATUS_NOTIFICATION_ID, UninitializedApp.STATUS_NOTIFICATION_ID,
UninitializedApp.get().buildStatusNotification(true)) UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction))
} catch (e: Exception) { } catch (e: Exception) {
TSLog.e(TAG, "Failed to start foreground service: $e") TSLog.e(TAG, "Failed to start foreground service: $e")
} }

Loading…
Cancel
Save