Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions android/src/main/java/com/tailscale/ipn/IPNReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions android/src/main/java/com/tailscale/ipn/StartVPNWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}
}
22 changes: 22 additions & 0 deletions android/src/main/java/com/tailscale/ipn/StopVPNWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
15 changes: 15 additions & 0 deletions android/src/main/java/com/tailscale/ipn/UseExitNodeWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
11 changes: 7 additions & 4 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<string name="not_connected">Not connected</string>
<string name="empty" translatable="false"> </string>
<string name="template" translatable="false">%s</string>
<string name="selected">Selected</string>
<string name="selected">Selected</string>
<string name="offline">Offline</string>
<string name="ok">OK</string>
<string name="_continue">Continue</string>
Expand All @@ -38,7 +38,7 @@
<string name="acknowledgements">Acknowledgements</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="terms_of_service">Terms of Service</string>
<string name="about_view_footnotes">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.</string>
<string name="about_view_footnotes">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.</string>
<string name="managed_by">Managed by</string>

<!-- Strings for the bug reporting screen -->
Expand Down Expand Up @@ -142,11 +142,11 @@
<string name="request_deletion_owner_part1">
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
</string>
<string name="request_deletion_owner_part2a">
<string name="request_deletion_owner_part2a">
and look for “Delete tailnet”.
</string>

<string name="request_deletion_owner_part2b">
<string name="request_deletion_owner_part2b">
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.
</string>

Expand Down Expand Up @@ -285,6 +285,9 @@
<string name="multiple_peers_with_name_found">Multiple peers with name %1$s found</string>
<string name="peer_with_name_is_not_an_exit_node">Peer with name %1$s is not an exit node</string>
<string name="use_exit_node_intent_failed">Use Exit Node Intent Failed</string>
<string name="starting_notification">Starting Tailscale VPN…</string>
<string name="stopping_notification">Stopping Tailscale VPN…</string>
<string name="changing_exit_node_notification">Changing exit node…</string>

<!-- Strings for the IPNReceiver -->
<string name="title_connection_failed">Tailscale Connection Failed</string>
Expand Down