diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 19fe620..bc0128f 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -4,12 +4,13 @@
-
+
+
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:foregroundServiceType="systemExempted">
diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt
index 8817fe0..90a9e24 100644
--- a/android/src/main/java/com/tailscale/ipn/App.kt
+++ b/android/src/main/java/com/tailscale/ipn/App.kt
@@ -6,6 +6,7 @@ import android.Manifest
import android.app.Activity
import android.app.Application
import android.app.Fragment
+import android.app.Notification
import android.app.NotificationChannel
import android.app.PendingIntent
import android.content.Context
@@ -50,8 +51,6 @@ class App : UninitializedApp(), libtailscale.AppContext {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
companion object {
- const val STATUS_CHANNEL_ID = "tailscale-status"
- const val STATUS_NOTIFICATION_ID = 1
private const val PEER_TAG = "peer"
private const val FILE_CHANNEL_ID = "tailscale-files"
private const val TAG = "App"
@@ -93,6 +92,10 @@ class App : UninitializedApp(), libtailscale.AppContext {
override fun onCreate() {
super.onCreate()
+ createNotificationChannel(
+ STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW)
+ createNotificationChannel(
+ FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT)
appInstance = this
setUnprotectedInstance(this)
}
@@ -125,10 +128,6 @@ class App : UninitializedApp(), libtailscale.AppContext {
Notifier.start(applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
setAndRegisterNetworkCallbacks()
- createNotificationChannel(
- STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW)
- createNotificationChannel(
- FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT)
applicationScope.launch {
Notifier.state.collect { state ->
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
@@ -221,20 +220,7 @@ class App : UninitializedApp(), libtailscale.AppContext {
setAbleToStartVPN(ableToStartVPN)
QuickToggleService.updateTile()
Log.d("App", "Set Tile Ready: $ableToStartVPN")
- val action = if (ableToStartVPN) IPNService.ACTION_STOP_VPN else IPNService.ACTION_START_VPN
- val intent = Intent(this, IPNService::class.java).apply { this.action = action }
- val pendingIntent: PendingIntent =
- PendingIntent.getBroadcast(
- this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- val notificationMessage =
- if (vpnRunning) getString(R.string.connected) else getString(R.string.not_connected)
- notify(
- "Tailscale",
- notificationMessage,
- vpnRunning,
- STATUS_CHANNEL_ID,
- pendingIntent,
- STATUS_NOTIFICATION_ID)
+ notifyStatus(vpnRunning)
}
override fun getModelName(): String {
@@ -362,6 +348,9 @@ class App : UninitializedApp(), libtailscale.AppContext {
*/
open class UninitializedApp : Application() {
companion object {
+ const val STATUS_NOTIFICATION_ID = 1
+ const val STATUS_CHANNEL_ID = "tailscale-status"
+
// Key for shared preference that tracks whether or not we're able to start
// the VPN (i.e. we're logged in and machine is authorized).
private const val ABLE_TO_START_VPN_KEY = "ableToStartVPN"
@@ -396,7 +385,7 @@ open class UninitializedApp : Application() {
fun startVPN() {
val intent = Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_START_VPN }
- startService(intent)
+ startForegroundService(intent)
}
fun stopVPN() {
@@ -410,26 +399,7 @@ open class UninitializedApp : Application() {
nm.createNotificationChannel(channel)
}
- fun notify(
- title: String?,
- message: String?,
- usesEnabledIcon: Boolean,
- channel: String,
- intent: PendingIntent?,
- notificationID: Int
- ) {
- val icon =
- if (usesEnabledIcon) R.drawable.ic_notification else R.drawable.ic_notification_disabled
- val builder: NotificationCompat.Builder =
- NotificationCompat.Builder(this, channel)
- .setSmallIcon(icon)
- .setContentTitle(title)
- .setContentText(message)
- .setContentIntent(intent)
- .setAutoCancel(true)
- .setOnlyAlertOnce(true)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
+ protected fun notifyStatus(vpnRunning: Boolean) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
@@ -441,6 +411,30 @@ open class UninitializedApp : Application() {
// for ActivityCompat#requestPermissions for more details.
return
}
- nm.notify(notificationID, builder.build())
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
+ nm.notify(STATUS_NOTIFICATION_ID, buildStatusNotification(vpnRunning))
+ }
+
+ fun buildStatusNotification(vpnRunning: 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 =
+ if (vpnRunning) IPNReceiver.INTENT_DISCONNECT_VPN else IPNReceiver.INTENT_CONNECT_VPN
+ val actionLabel = getString(if (vpnRunning) R.string.disconnect else R.string.connect)
+ val intent = Intent(this, IPNReceiver::class.java).apply { this.action = action }
+ val pendingIntent: PendingIntent =
+ PendingIntent.getBroadcast(
+ this, 0, 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)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingIntent).build())
+ .build()
}
}
diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt
index f050334..f64a2e2 100644
--- a/android/src/main/java/com/tailscale/ipn/IPNService.kt
+++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt
@@ -32,6 +32,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
START_NOT_STICKY
}
ACTION_START_VPN -> {
+ showForegroundNotification()
App.get().setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
@@ -72,6 +73,12 @@ open class IPNService : VpnService(), libtailscale.IPNService {
super.onRevoke()
}
+ private fun showForegroundNotification() {
+ startForeground(
+ UninitializedApp.STATUS_NOTIFICATION_ID,
+ UninitializedApp.get().buildStatusNotification(true))
+ }
+
private fun configIntent(): PendingIntent {
return PendingIntent.getActivity(
this,
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index da52c90..cee824e 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Log out
None
Connect
+ Disconnect
Unknown user
Connected
Not connected