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
23 changes: 21 additions & 2 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
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 @@ -47,6 +51,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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that this might cause us to skip important updates while the cached state is stale, like when the app is backgrounded. If vpnActive doesn't reflect the current backend state when we get an external broadcast, this would cause us to skip handling the intent

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 @@ -57,8 +68,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.WORKER_LEGACY_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(app.getString(R.string.starting_notification))
.setPriority(NotificationCompat.PRIORITY_MIN)
.build();

return new ForegroundInfo(UninitializedApp.STATUS_WORKER_LEGACY_NOTIFICATION_ID, notification);
}
}
24 changes: 24 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,17 @@

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;
import androidx.core.app.NotificationCompat;
import androidx.work.ForegroundInfo;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

Expand All @@ -26,4 +34,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.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_WORKER_LEGACY_NOTIFICATION_ID, notification);
}
}
Loading