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.
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