// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.WorkerParameters import com.tailscale.ipn.UninitializedApp.Companion.STATUS_CHANNEL_ID 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 class UseExitNodeWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { val app = UninitializedApp.get() ?: return Result.failure() suspend fun runAndGetResult(): String? { val exitNodeName = inputData.getString(EXIT_NODE_NAME) val exitNodeId = if (exitNodeName.isNullOrEmpty()) { null } else { if (app.isAbleToStartVPN() != true) { return app.getString(R.string.vpn_is_not_ready_to_start) } 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 filteredPeers = peers.filter { it.displayName == exitNodeName }.toList() 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].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 } } scope.coroutineContext[Job]?.join() return result } val result = runAndGetResult() 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() } } companion object { const val EXIT_NODE_NAME = "EXIT_NODE_NAME" const val ALLOW_LAN_ACCESS = "ALLOW_LAN_ACCESS" const val ERROR_KEY = "error" } }