From 179e75bd20a8372e0f422da832788a2be91087d3 Mon Sep 17 00:00:00 2001 From: Pawloland <59684145+Pawloland@users.noreply.github.com> Date: Mon, 18 May 2026 13:32:00 +0200 Subject: [PATCH 1/3] Fix expedited WorkRequests by implementing getForegroundInfo() method for all Workers, thus fixing intent handling on Android 11 and older Fixes tailscale/tailscale#19772 Signed-off-by: Pawloland <59684145+Pawloland@users.noreply.github.com> --- .../java/com/tailscale/ipn/IPNReceiver.java | 26 +++++++++++++++++++ .../com/tailscale/ipn/StartVPNWorker.java | 21 +++++++++++++++ .../java/com/tailscale/ipn/StopVPNWorker.java | 22 ++++++++++++++++ .../com/tailscale/ipn/UseExitNodeWorker.kt | 15 +++++++++++ android/src/main/res/values/strings.xml | 11 +++++--- 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/IPNReceiver.java b/android/src/main/java/com/tailscale/ipn/IPNReceiver.java index 87ab33c023..5db2f65746 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNReceiver.java +++ b/android/src/main/java/com/tailscale/ipn/IPNReceiver.java @@ -13,6 +13,10 @@ import androidx.work.OutOfQuotaPolicy; import androidx.work.WorkManager; +import com.tailscale.ipn.ui.model.Ipn; +import com.tailscale.ipn.ui.model.Netmap; +import com.tailscale.ipn.ui.notifier.Notifier; + import java.util.Objects; /** @@ -46,6 +50,13 @@ public void onReceive(Context context, Intent intent) { workManager.enqueueUniqueWork(WORK_CONNECT, ExistingWorkPolicy.REPLACE, req); } else if (Objects.equals(action, INTENT_DISCONNECT_VPN)) { + // If we're already disconnected, skip triggering the worker to avoid overwriting the status notification + // with the "Stopping Tailscale VPN…" one. + boolean running = UninitializedApp.get().getAppScopedViewModel().getVpnActive().getValue(); + if (!running) { + return; + } + OneTimeWorkRequest req = new OneTimeWorkRequest.Builder(StopVPNWorker.class) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) @@ -56,8 +67,23 @@ public void onReceive(Context context, Intent intent) { } else if (Objects.equals(action, INTENT_USE_EXIT_NODE)) { String exitNode = intent.getStringExtra("exitNode"); + if (exitNode != null && exitNode.isEmpty()) exitNode = null; boolean allowLanAccess = intent.getBooleanExtra("allowLanAccess", false); + + Ipn.Prefs currentPrefs = Notifier.INSTANCE.getPrefs().getValue(); + Netmap.NetworkMap currentNetmap = Notifier.INSTANCE.getNetmap().getValue(); + String currentExitNodeName = UninitializedApp.Companion.getExitNodeName(currentPrefs, currentNetmap); + boolean currentAllowLan = false; + if (currentPrefs != null) { + currentAllowLan = currentPrefs.getExitNodeAllowLANAccess(); + } + // If the exit node configuration is the same as requested, skip triggering the worker + // to avoid overwriting the status notification with the "Changing exit node…" one. + if (Objects.equals(exitNode, currentExitNodeName) && allowLanAccess == currentAllowLan) { + return; + } + Data input = new Data.Builder() .putString(UseExitNodeWorker.EXIT_NODE_NAME, exitNode) diff --git a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java index 9ab4183a5a..bd80760a6d 100644 --- a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java @@ -3,6 +3,9 @@ package com.tailscale.ipn; +import static com.tailscale.ipn.UninitializedApp.STATUS_NOTIFICATION_ID; + +import android.app.Application; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -12,6 +15,8 @@ import android.os.Build; import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.ForegroundInfo; import androidx.work.Worker; import androidx.work.WorkerParameters; @@ -62,4 +67,20 @@ public Result doWork() { return Result.failure(); } + + @NonNull + @Override + public ForegroundInfo getForegroundInfo() { + // notification just so that there is no exception on android 11 and older (api 30 and older) + // it will be only briefly visible in the real world because the intent finishes almost instantly + // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat + Application app = UninitializedApp.get(); + Notification notification = new NotificationCompat.Builder(app, UninitializedApp.STATUS_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(app.getString(R.string.starting_notification)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build(); + + return new ForegroundInfo(STATUS_NOTIFICATION_ID, notification); + } } diff --git a/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java index 7bb2e172db..2fca971be4 100644 --- a/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java @@ -3,9 +3,15 @@ package com.tailscale.ipn; +import static com.tailscale.ipn.UninitializedApp.STATUS_NOTIFICATION_ID; + +import android.app.Application; +import android.app.Notification; import android.content.Context; import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.ForegroundInfo; import androidx.work.Worker; import androidx.work.WorkerParameters; @@ -26,4 +32,20 @@ public Result doWork() { UninitializedApp.get().stopVPN(); return Result.success(); } + + @NonNull + @Override + public ForegroundInfo getForegroundInfo() { + // notification just so that there is no exception on android 11 and older (api 30 and older) + // it will be only briefly visible in the real world because the intent finishes almost instantly + // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat + Application app = UninitializedApp.get(); + Notification notification = new NotificationCompat.Builder(app, UninitializedApp.STATUS_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(app.getString(R.string.stopping_notification)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build(); + + return new ForegroundInfo(STATUS_NOTIFICATION_ID, notification); + } } diff --git a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt index e2b2bbc0d1..5c9432ec8c 100644 --- a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt +++ b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt @@ -8,8 +8,10 @@ import android.content.Intent import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker import androidx.work.Data +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.tailscale.ipn.UninitializedApp.Companion.STATUS_CHANNEL_ID +import com.tailscale.ipn.UninitializedApp.Companion.STATUS_NOTIFICATION_ID import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier @@ -104,6 +106,19 @@ class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : } } + override suspend fun getForegroundInfo(): ForegroundInfo { + // notification just so that there is no exception on android 11 and older (api 30 and older) + // it will be only briefly visible in the real world because the intent finishes almost instantly + // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat + val app = UninitializedApp.get() + val notification = + NotificationCompat.Builder(app, STATUS_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(app.getString(R.string.changing_exit_node_notification)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build() + return ForegroundInfo(STATUS_NOTIFICATION_ID, notification) + } companion object { const val EXIT_NODE_NAME = "EXIT_NODE_NAME" const val ALLOW_LAN_ACCESS = "ALLOW_LAN_ACCESS" diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 608f1f2a2e..a3904f2374 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Not connected %s - Selected + Selected Offline OK Continue @@ -38,7 +38,7 @@ Acknowledgements Privacy Policy Terms of Service - WireGuard is a registered trademark of Jason A. Donenfeld.\n\n© 2024 Tailscale Inc. All rights reserved.\nTailscale is a registered trademark of Tailscale Inc. + WireGuard is a registered trademark of Jason A. Donenfeld.\n\n© 2026 Tailscale Inc. All rights reserved.\nTailscale is a registered trademark of Tailscale Inc. Managed by @@ -142,11 +142,11 @@ As the owner of this tailnet, to remove yourself from the tailnet you can either reassign ownership and contact our Support team, or delete the whole tailnet through the admin console. To do the latter, go to - + and look for “Delete tailnet”. - + All requests related to the removal or deletion of data are handled by our Support team. To open a request, tap the Contact Support button below to be taken to our contact form in the browser. Complete the form, and a Customer Support Engineer will work with you directly to assist. @@ -285,6 +285,9 @@ Multiple peers with name %1$s found Peer with name %1$s is not an exit node Use Exit Node Intent Failed + Starting Tailscale VPN… + Stopping Tailscale VPN… + Changing exit node… Tailscale Connection Failed From 47c06216612f0c11868767b98a815647395a80a9 Mon Sep 17 00:00:00 2001 From: Pawloland <59684145+Pawloland@users.noreply.github.com> Date: Wed, 20 May 2026 16:39:15 +0200 Subject: [PATCH 2/3] Edit UseExitNodeWorker to fix foreground notification becoming non-dismissible on Android 11 and older when triggering USE_EXIT_NODE intent Signed-off-by: Pawloland <59684145+Pawloland@users.noreply.github.com> --- .../com/tailscale/ipn/UseExitNodeWorker.kt | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt index 5c9432ec8c..0fa8de0dfa 100644 --- a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt +++ b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt @@ -16,69 +16,85 @@ import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlin.coroutines.resume class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result { - val app = UninitializedApp.get() - suspend fun runAndGetResult(): String? { - val exitNodeName = inputData.getString(EXIT_NODE_NAME) - - val exitNodeId = - if (exitNodeName.isNullOrEmpty()) { - null - } else { - if (!app.isAbleToStartVPN()) { - return app.getString(R.string.vpn_is_not_ready_to_start) - } + override suspend fun doWork(): Result { + val app = UninitializedApp.get() - val peers = - (Notifier.netmap.value - ?: run { - return@runAndGetResult app.getString(R.string.tailscale_is_not_setup) - }) - .Peers - ?: run { - return@runAndGetResult app.getString(R.string.no_peers_found) + val exitNodeName = inputData.getString(EXIT_NODE_NAME) + + val exitNodeId = + if (exitNodeName.isNullOrEmpty()) { + null + } else { + if (!app.isAbleToStartVPN()) { + return Result.failure( + Data.Builder().putString( + ERROR_KEY, app.getString(R.string.vpn_is_not_ready_to_start) + ).build() + ) + } + + val netmap = Notifier.netmap.value ?: return Result.failure( + Data.Builder().putString( + ERROR_KEY, app.getString(R.string.tailscale_is_not_setup) + ).build() + ) + + val peers = netmap.Peers ?: return Result.failure( + Data.Builder().putString( + ERROR_KEY, app.getString(R.string.no_peers_found) + ).build() + ) + + + val filteredPeers = peers.filter { it.displayName == exitNodeName }.toList() + + when { + filteredPeers.isEmpty() -> { + return Result.failure( + Data.Builder().putString(ERROR_KEY, app.getString(R.string.no_peers_with_name_found, exitNodeName)).build() + ) } - val filteredPeers = peers.filter { it.displayName == exitNodeName }.toList() + filteredPeers.size > 1 -> { + return Result.failure( + Data.Builder().putString(ERROR_KEY, app.getString(R.string.multiple_peers_with_name_found, exitNodeName)).build() + ) + } - if (filteredPeers.isEmpty()) { - return app.getString(R.string.no_peers_with_name_found, exitNodeName) - } else if (filteredPeers.size > 1) { - return app.getString(R.string.multiple_peers_with_name_found, exitNodeName) - } else if (!filteredPeers[0].isExitNode) { - return app.getString(R.string.peer_with_name_is_not_an_exit_node, exitNodeName) + !filteredPeers[0].isExitNode -> { + return Result.failure( + Data.Builder().putString(ERROR_KEY, app.getString(R.string.peer_with_name_is_not_an_exit_node, exitNodeName)).build() + ) + } + } + filteredPeers[0].StableID } - filteredPeers[0].StableID - } - - val allowLanAccess = inputData.getBoolean(ALLOW_LAN_ACCESS, false) - val prefsOut = Ipn.MaskedPrefs() - prefsOut.ExitNodeID = exitNodeId - prefsOut.ExitNodeAllowLANAccess = allowLanAccess - - val scope = CoroutineScope(Dispatchers.Default + Job()) - var result: String? = null - Client(scope).editPrefs(prefsOut) { - result = - if (it.isFailure) { - it.exceptionOrNull()?.message - } else { - null + val allowLanAccess = inputData.getBoolean(ALLOW_LAN_ACCESS, false) + val prefsOut = Ipn.MaskedPrefs() + prefsOut.ExitNodeID = exitNodeId + prefsOut.ExitNodeAllowLANAccess = allowLanAccess + + val scope = CoroutineScope(kotlinx.coroutines.currentCoroutineContext()) + + val result: String? = + kotlinx.coroutines.suspendCancellableCoroutine { cont -> + Client(scope).editPrefs(prefsOut) { editResult -> + val err = + if (editResult.isFailure) { + editResult.exceptionOrNull()?.message + } else { + null + } + if (cont.isActive) { + cont.resume(err) + } + } } - } - - scope.coroutineContext[Job]?.join() - - return result - } - - val result = runAndGetResult() return if (result != null) { val intent = From a9b74feba4197e436950ec98c38b17b58387978a Mon Sep 17 00:00:00 2001 From: Pawloland <59684145+Pawloland@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:54:39 +0200 Subject: [PATCH 3/3] Add separate channels for errors Errors reported in Workers are now in separate channels using separate ID, so they can be disabled in system settings, while keeping the main status notifications intact (as the state doesn't change when error occurs). Also add separate channel for notifications that are visible when starting an intent from 3rd party integrations or shell, on devices older than Android 12. They also can be disabled in settings, so the app will behave like in newer Android versions while not overriding main status notification. Signed-off-by: Pawloland <59684145+Pawloland@users.noreply.github.com> --- .../src/main/java/com/tailscale/ipn/App.kt | 23 ++- .../com/tailscale/ipn/StartVPNWorker.java | 4 +- .../java/com/tailscale/ipn/StopVPNWorker.java | 6 +- .../com/tailscale/ipn/UseExitNodeWorker.kt | 133 +++++++++++------- android/src/main/res/values/strings.xml | 4 + 5 files changed, 110 insertions(+), 60 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 2d3ba020f3..01bf11b93a 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -117,6 +117,19 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { getString(R.string.vpn_status), getString(R.string.optional_notifications_which_display_the_status_of_the_vpn_tunnel), NotificationManagerCompat.IMPORTANCE_MIN) + createNotificationChannel( + STATUS_FAILURE_CHANNEL_ID, + getString(R.string.intent_failure_channel_name), + getString(R.string.intent_failure_channel_description), + NotificationManagerCompat.IMPORTANCE_MIN) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + createNotificationChannel( + WORKER_LEGACY_CHANNEL_ID, + getString(R.string.intent_status_channel_name), + getString(R.string.intent_status_channel_description), + NotificationManagerCompat.IMPORTANCE_MIN + ) + } createNotificationChannel( FILE_CHANNEL_ID, getString(R.string.taildrop_file_transfers), @@ -507,7 +520,10 @@ open class UninitializedApp : Application() { const val TAG = "UninitializedApp" const val STATUS_NOTIFICATION_ID = 1 const val STATUS_EXIT_NODE_FAILURE_NOTIFICATION_ID = 2 + const val STATUS_WORKER_LEGACY_NOTIFICATION_ID = 3 const val STATUS_CHANNEL_ID = "tailscale-status" + const val STATUS_FAILURE_CHANNEL_ID = "tailscale-status-failure" + const val WORKER_LEGACY_CHANNEL_ID = "tailscale-worker-legacy" // 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" @@ -641,7 +657,7 @@ open class UninitializedApp : Application() { notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction, exitNodeName)) } - fun notifyStatus(notification: Notification) { + fun notifyStatus(notification: Notification, exitNodeFailure: Boolean = false) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling @@ -653,7 +669,10 @@ open class UninitializedApp : Application() { // for ActivityCompat#requestPermissions for more details. return } - notificationManager.notify(STATUS_NOTIFICATION_ID, notification) + notificationManager.notify( + if (exitNodeFailure) STATUS_EXIT_NODE_FAILURE_NOTIFICATION_ID else STATUS_NOTIFICATION_ID, + notification + ) } fun buildStatusNotification( diff --git a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java index bd80760a6d..b44bca4301 100644 --- a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java @@ -75,12 +75,12 @@ public ForegroundInfo getForegroundInfo() { // it will be only briefly visible in the real world because the intent finishes almost instantly // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat Application app = UninitializedApp.get(); - Notification notification = new NotificationCompat.Builder(app, UninitializedApp.STATUS_CHANNEL_ID) + Notification notification = new NotificationCompat.Builder(app, UninitializedApp.WORKER_LEGACY_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(app.getString(R.string.starting_notification)) .setPriority(NotificationCompat.PRIORITY_MIN) .build(); - return new ForegroundInfo(STATUS_NOTIFICATION_ID, notification); + return new ForegroundInfo(UninitializedApp.STATUS_WORKER_LEGACY_NOTIFICATION_ID, notification); } } diff --git a/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java index 2fca971be4..895ffbe465 100644 --- a/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StopVPNWorker.java @@ -4,9 +4,11 @@ package com.tailscale.ipn; import static com.tailscale.ipn.UninitializedApp.STATUS_NOTIFICATION_ID; +import static com.tailscale.ipn.UninitializedApp.STATUS_WORKER_LEGACY_NOTIFICATION_ID; import android.app.Application; import android.app.Notification; +import android.app.NotificationManager; import android.content.Context; import androidx.annotation.NonNull; @@ -40,12 +42,12 @@ public ForegroundInfo getForegroundInfo() { // it will be only briefly visible in the real world because the intent finishes almost instantly // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat Application app = UninitializedApp.get(); - Notification notification = new NotificationCompat.Builder(app, UninitializedApp.STATUS_CHANNEL_ID) + Notification notification = new NotificationCompat.Builder(app, UninitializedApp.WORKER_LEGACY_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(app.getString(R.string.stopping_notification)) .setPriority(NotificationCompat.PRIORITY_MIN) .build(); - return new ForegroundInfo(STATUS_NOTIFICATION_ID, notification); + return new ForegroundInfo(STATUS_WORKER_LEGACY_NOTIFICATION_ID, notification); } } diff --git a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt index 0fa8de0dfa..9aa80a625d 100644 --- a/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt +++ b/android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt @@ -10,8 +10,10 @@ import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.ForegroundInfo import androidx.work.WorkerParameters -import com.tailscale.ipn.UninitializedApp.Companion.STATUS_CHANNEL_ID -import com.tailscale.ipn.UninitializedApp.Companion.STATUS_NOTIFICATION_ID +import com.tailscale.ipn.UninitializedApp.Companion.STATUS_FAILURE_CHANNEL_ID +import com.tailscale.ipn.UninitializedApp.Companion.STATUS_WORKER_LEGACY_NOTIFICATION_ID +import com.tailscale.ipn.UninitializedApp.Companion.WORKER_LEGACY_CHANNEL_ID +import com.tailscale.ipn.UninitializedApp.Companion.get import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier @@ -21,7 +23,7 @@ import kotlin.coroutines.resume class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { - val app = UninitializedApp.get() + val app = get() val exitNodeName = inputData.getString(EXIT_NODE_NAME) @@ -30,44 +32,57 @@ class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : null } else { if (!app.isAbleToStartVPN()) { - return Result.failure( - Data.Builder().putString( - ERROR_KEY, app.getString(R.string.vpn_is_not_ready_to_start) - ).build() + return failure( + app, + app.getString(R.string.vpn_is_not_ready_to_start), ) } - val netmap = Notifier.netmap.value ?: return Result.failure( - Data.Builder().putString( - ERROR_KEY, app.getString(R.string.tailscale_is_not_setup) - ).build() - ) + val netmap = + Notifier.netmap.value + ?: return failure( + app, + app.getString(R.string.tailscale_is_not_setup), + ) - val peers = netmap.Peers ?: return Result.failure( - Data.Builder().putString( - ERROR_KEY, app.getString(R.string.no_peers_found) - ).build() - ) + val peers = + netmap.Peers + ?: return failure( + app, + app.getString(R.string.no_peers_found), + ) val filteredPeers = peers.filter { it.displayName == exitNodeName }.toList() when { filteredPeers.isEmpty() -> { - return Result.failure( - Data.Builder().putString(ERROR_KEY, app.getString(R.string.no_peers_with_name_found, exitNodeName)).build() + return failure( + app, + app.getString( + R.string.no_peers_with_name_found, + exitNodeName, + ), ) } filteredPeers.size > 1 -> { - return Result.failure( - Data.Builder().putString(ERROR_KEY, app.getString(R.string.multiple_peers_with_name_found, exitNodeName)).build() + return failure( + app, + app.getString( + R.string.multiple_peers_with_name_found, + exitNodeName, + ), ) } !filteredPeers[0].isExitNode -> { - return Result.failure( - Data.Builder().putString(ERROR_KEY, app.getString(R.string.peer_with_name_is_not_an_exit_node, exitNodeName)).build() + return failure( + app, + app.getString( + R.string.peer_with_name_is_not_an_exit_node, + exitNodeName, + ), ) } } @@ -96,31 +111,40 @@ class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : } } - return if (result != null) { - val intent = - Intent(app, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - val pendingIntent: PendingIntent = - PendingIntent.getActivity( - app, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - - val notification = - NotificationCompat.Builder(app, STATUS_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(app.getString(R.string.use_exit_node_intent_failed)) - .setContentText(result) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(pendingIntent) - .build() - - app.notifyStatus(notification) - - Result.failure(Data.Builder().putString(ERROR_KEY, result).build()) - } else { - Result.success() + return if (result != null) { + failure(app, result) + } else { + Result.success() + } + } + + private fun failure( + app: UninitializedApp, + result: String, + ): Result { + val intent = + Intent(app, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val pendingIntent: PendingIntent = + PendingIntent.getActivity( + app, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = + NotificationCompat.Builder(app, STATUS_FAILURE_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(app.getString(R.string.use_exit_node_intent_failed)) + .setContentText(result) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setSilent(true) + .build() + + app.notifyStatus(notification, true) + return Result.failure(Data.Builder().putString(ERROR_KEY, result).build()) } - } override suspend fun getForegroundInfo(): ForegroundInfo { // notification just so that there is no exception on android 11 and older (api 30 and older) @@ -128,16 +152,17 @@ class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : // https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat val app = UninitializedApp.get() val notification = - NotificationCompat.Builder(app, STATUS_CHANNEL_ID) + NotificationCompat.Builder(app, WORKER_LEGACY_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(app.getString(R.string.changing_exit_node_notification)) .setPriority(NotificationCompat.PRIORITY_MIN) .build() - return ForegroundInfo(STATUS_NOTIFICATION_ID, notification) + return ForegroundInfo(STATUS_WORKER_LEGACY_NOTIFICATION_ID, notification) + } + + companion object { + const val EXIT_NODE_NAME = "EXIT_NODE_NAME" + const val ALLOW_LAN_ACCESS = "ALLOW_LAN_ACCESS" + const val ERROR_KEY = "error" } - companion object { - const val EXIT_NODE_NAME = "EXIT_NODE_NAME" - const val ALLOW_LAN_ACCESS = "ALLOW_LAN_ACCESS" - const val ERROR_KEY = "error" - } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index fc8b66dbe9..867db2dbb1 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -298,6 +298,10 @@ Taildrop file transfers VPN status + Intents failures + Notifications about failures when managing VPN from Intents (3rd party integration) + Intents status + Notifications about a status when managing VPN from Intents (3rd party integration) on Android 11 and older (You can disable this channel) VPN start Notifications delivered when user interaction is required to establish the VPN tunnel. Optional notifications which display the status of the VPN tunnel.