android: add persistent notification with VPN status (#362)

-When connected, tapping on the notification disconnects
-When disconnected, tapping on the notification connects
-Navigate to system notifications instead of app info when tapping on 'Notifications'
-Clean up unused notification channel and methods

Fixes tailscale/tailscale#10104

Signed-off-by: kari-ts <kari@tailscale.com>
pull/363/head
kari-ts 2 months ago committed by GitHub
parent d330726ba1
commit 5e3236260f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -61,11 +61,9 @@ class App : Application(), libtailscale.AppContext {
companion object {
const val STATUS_CHANNEL_ID = "tailscale-status"
const val STATUS_NOTIFICATION_ID = 1
const val NOTIFY_CHANNEL_ID = "tailscale-notify"
const val NOTIFY_NOTIFICATION_ID = 2
private const val PEER_TAG = "peer"
private const val FILE_CHANNEL_ID = "tailscale-files"
private const val FILE_NOTIFICATION_ID = 3
private const val FILE_NOTIFICATION_ID = 2
private const val TAG = "App"
private val networkConnectivityRequest =
NetworkRequest.Builder()
@ -114,15 +112,13 @@ class App : Application(), libtailscale.AppContext {
Notifier.start(applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
setAndRegisterNetworkCallbacks()
createNotificationChannel(
NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT)
createNotificationChannel(
STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW)
createNotificationChannel(
FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT)
appInstance = this
applicationScope.launch {
Notifier.tileActive.collect { isTileReadyToBeActive -> setTileActive(isTileReadyToBeActive) }
Notifier.connStatus.collect { connStatus -> updateConnStatus(connStatus) }
}
}
@ -212,15 +208,33 @@ class App : Application(), libtailscale.AppContext {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
}
fun setTileActive(ready: Boolean) {
fun updateConnStatus(ready: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return
}
QuickToggleService.setReady(this, ready)
Log.d("App", "Set Tile Ready: $ready")
val action = if (ready) IPNReceiver.INTENT_DISCONNECT_VPN else IPNReceiver.INTENT_CONNECT_VPN
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
)
if (ready){
startVPN()
}
val notificationMessage = if (ready) getString(R.string.connected) else getString(R.string.not_connected)
notify(
"Tailscale",
notificationMessage,
STATUS_CHANNEL_ID,
pendingIntent,
STATUS_NOTIFICATION_ID
)
}
fun getHostname(): String {
@ -323,12 +337,22 @@ class App : Application(), libtailscale.AppContext {
}
val pending: PendingIntent =
PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT)
notify(getString(R.string.file_notification), msg, FILE_CHANNEL_ID, pending, FILE_NOTIFICATION_ID)
}
fun createNotificationChannel(id: String?, name: String?, importance: Int) {
val channel = NotificationChannel(id, name, importance)
val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
nm.createNotificationChannel(channel)
}
fun notify(title: String?, message: String?, channel: String, intent: PendingIntent?, notificationID: Int) {
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, FILE_CHANNEL_ID)
NotificationCompat.Builder(this, channel)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("File received")
.setContentText(msg)
.setContentIntent(pending)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@ -344,13 +368,7 @@ class App : Application(), libtailscale.AppContext {
// for ActivityCompat#requestPermissions for more details.
return
}
nm.notify(FILE_NOTIFICATION_ID, builder.build())
}
fun createNotificationChannel(id: String?, name: String?, importance: Int) {
val channel = NotificationChannel(id, name, importance)
val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
nm.createNotificationChannel(channel)
nm.notify(notificationID, builder.build())
}
override fun getInterfacesAsString(): String {

@ -97,32 +97,8 @@ open class IPNService : VpnService(), libtailscale.IPNService {
return VPNServiceBuilder(b)
}
fun notify(title: String?, message: String?) {
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(configIntent())
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build())
}
fun updateStatusNotification(title: String?, message: String?) {
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(configIntent())
.setPriority(NotificationCompat.PRIORITY_LOW)
startForeground(App.STATUS_NOTIFICATION_ID, builder.build())
}
companion object {
const val ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN"
const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"
}
}

@ -16,6 +16,7 @@ import android.net.VpnService
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
@ -348,9 +349,9 @@ class MainActivity : ComponentActivity() {
private fun openApplicationSettings() {
val intent =
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null))
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}

@ -31,7 +31,7 @@ object Notifier {
private val decoder = Json { ignoreUnknownKeys = true }
// Global App State
val tileActive: StateFlow<Boolean> = MutableStateFlow(false)
val connStatus: StateFlow<Boolean> = MutableStateFlow(false)
val readyToPrepareVPN: StateFlow<Boolean> = MutableStateFlow(false)
// General IPN Bus State
@ -82,7 +82,7 @@ object Notifier {
}
state.collect { currstate ->
readyToPrepareVPN.set(currstate > Ipn.State.Stopped)
tileActive.set(currstate > Ipn.State.Stopped)
connStatus.set(currstate > Ipn.State.Stopped)
}
}
}

@ -212,7 +212,7 @@
<string name="permission_write_external_storage">Storage</string>
<string name="permission_write_external_storage_needed">We use storage in order to receive files with Taildrop.</string>
<string name="permission_post_notifications">Notifications</string>
<string name="permission_post_notifications_needed">We use notifications to help you troubleshoot broken connections, or notify you before you need to reauthenticate to your network.</string>
<string name="permission_post_notifications_needed">We use notifications to help you troubleshoot broken connections, or notify you before you need to reauthenticate to your network. Persistent status notifications are off by default and can be enabled in system settings. </string>
<!-- Strings for the share activity -->
@ -226,6 +226,7 @@
<string name="taildrop_share_failed">Taildrop failed. Some files were not shared. Please try again.</string>
<string name="taildrop_sending">Sending</string>
<string name="taildrop_share_failed_short">Failed</string>
<string name="file_notification">File received</string>
<!-- Error Dialog Titles -->

Loading…
Cancel
Save