From 40090f179bb1afb0992a3ed9045dafae3754f266 Mon Sep 17 00:00:00 2001 From: Josh Vocal Date: Fri, 23 Aug 2024 14:00:10 -0700 Subject: [PATCH 01/91] android: Fix search not filtering machines from input (#478) android: Fix search not filtering text input Fixes tailscale/tailscale#13218 * Filtering machines in the textfield works since the flow is now reachable * Updating the health icon works since the flow is now reachable Signed-off-by: Josh Vocal --- .../ipn/ui/viewModel/MainViewModel.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index e583c1a..3ce6a30 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -23,9 +23,11 @@ import com.tailscale.ipn.ui.util.PeerCategorizer import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.set +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import java.time.Duration @@ -38,6 +40,7 @@ class MainViewModelFactory(private val vpnViewModel: VpnViewModel) : ViewModelPr } } +@OptIn(FlowPreview::class) class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { // The user readable state of the system @@ -112,6 +115,14 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } } + viewModelScope.launch { + searchTerm + .debounce(250L) + .collect { term -> + peers.set(peerCategorizer.groupedAndFilteredPeers(term)) + } + } + viewModelScope.launch { Notifier.netmap.collect { it -> it?.let { netmap -> @@ -131,14 +142,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } } } + } - viewModelScope.launch { - searchTerm.collect { term -> peers.set(peerCategorizer.groupedAndFilteredPeers(term)) } - } - - viewModelScope.launch { - App.get().healthNotifier?.currentIcon?.collect { icon -> healthIcon.set(icon) } - } + viewModelScope.launch { + App.get().healthNotifier?.currentIcon?.collect { icon -> healthIcon.set(icon) } } } From 29e3c187c21f899e47b3f3b910fb9711f32e9323 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:45:22 -0700 Subject: [PATCH 02/91] android: stop tailscaled when VPN has been revoked (#480) -add new Ipn UI state 'Stopping' to handle the case where the VPN is no longer active and a request to stop Tailscale has been issued (but is not complete yet) and use for optimistic UI -when VPN has been revoked, stop tailscaled and set the state to Stopping -this fixes the race condition where when we tell tailscaled to stop, stopping races against the netmap state updating as a result of the VPN being revoked -add isActive state and use instead of isPrepared for UI showing whether we are connected - we were previously using isPrepared as a proxy for connection, but sometimes the VPN has been prepared but is not active (eg when VPN permissions have been given and VPN has been connected previously, but has been revoked) -refactor network callbacks into its own class for readability Fixes tailscale/tailscale#12850 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 60 +++---------------- .../main/java/com/tailscale/ipn/IPNService.kt | 23 +++++-- .../java/com/tailscale/ipn/MainActivity.kt | 4 +- .../tailscale/ipn/NetworkChangeCallback.kt | 58 ++++++++++++++++++ .../java/com/tailscale/ipn/ui/model/Ipn.kt | 6 +- .../com/tailscale/ipn/ui/notifier/Notifier.kt | 39 ++++++------ .../com/tailscale/ipn/ui/view/MainView.kt | 6 +- .../ipn/ui/viewModel/IpnViewModel.kt | 5 -- .../ipn/ui/viewModel/MainViewModel.kt | 32 +++++----- .../ipn/ui/viewModel/VpnViewModel.kt | 13 +++- go.mod | 2 +- libtailscale/backend.go | 21 ++++--- libtailscale/interfaces.go | 6 +- libtailscale/net.go | 35 ++++++++++- 14 files changed, 195 insertions(+), 115 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 3a1e88e..5df2cab 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -12,10 +12,6 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.ConnectivityManager -import android.net.LinkProperties -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkRequest import android.os.Build import android.os.Environment import android.util.Log @@ -45,7 +41,6 @@ import kotlinx.serialization.json.Json import libtailscale.Libtailscale import java.io.File import java.io.IOException -import java.net.InetAddress import java.net.NetworkInterface import java.security.GeneralSecurityException import java.util.Locale @@ -56,11 +51,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { companion object { private const val FILE_CHANNEL_ID = "tailscale-files" private const val TAG = "App" - private val networkConnectivityRequest = - NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .build() private lateinit var appInstance: App /** @@ -81,9 +71,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override val viewModelStore: ViewModelStore get() = appViewModelStore - lateinit var vpnViewModel: VpnViewModel - private set - private val appViewModelStore: ViewModelStore by lazy { ViewModelStore() } var healthNotifier: HealthNotifier? = null @@ -147,7 +134,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { Notifier.start(applicationScope) healthNotifier = HealthNotifier(Notifier.health, applicationScope) connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - setAndRegisterNetworkCallbacks() + NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns) + initViewModels() applicationScope.launch { Notifier.state.collect { state -> val ableToStartVPN = state > Ipn.State.NeedsMachineAuth @@ -161,14 +149,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { QuickToggleService.setVPNRunning(vpnRunning) } } - initViewModels() } private fun initViewModels() { vpnViewModel = ViewModelProvider(this, VpnViewModelFactory(this)).get(VpnViewModel::class.java) } - fun setWantRunning(wantRunning: Boolean) { + fun setWantRunning(wantRunning: Boolean, onSuccess: (() -> Unit)? = null) { val callback: (Result) -> Unit = { result -> result.fold( onSuccess = {}, @@ -180,41 +167,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { .editPrefs(Ipn.MaskedPrefs().apply { WantRunning = wantRunning }, callback) } - // requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is - // possible that this might return an unusuable network, eg a captive portal. - private fun setAndRegisterNetworkCallbacks() { - connectivityManager.requestNetwork( - networkConnectivityRequest, - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - super.onAvailable(network) - - val sb = StringBuilder() - val linkProperties: LinkProperties? = connectivityManager.getLinkProperties(network) - val dnsList: MutableList = linkProperties?.dnsServers ?: mutableListOf() - for (ip in dnsList) { - sb.append(ip.hostAddress).append(" ") - } - val searchDomains: String? = linkProperties?.domains - if (searchDomains != null) { - sb.append("\n") - sb.append(searchDomains) - } - - if (dns.updateDNSFromNetwork(sb.toString())) { - Libtailscale.onDNSConfigChanged(linkProperties?.interfaceName) - } - } - - override fun onLost(network: Network) { - super.onLost(network) - if (dns.updateDNSFromNetwork("")) { - Libtailscale.onDNSConfigChanged("") - } - } - }) - } - // encryptToPref a byte array of data using the Jetpack Security // library and writes it to a global encrypted preference store. @Throws(IOException::class, GeneralSecurityException::class) @@ -389,6 +341,8 @@ open class UninitializedApp : Application() { private lateinit var appInstance: UninitializedApp lateinit var notificationManager: NotificationManagerCompat + lateinit var vpnViewModel: VpnViewModel + @JvmStatic fun get(): UninitializedApp { return appInstance @@ -550,6 +504,10 @@ open class UninitializedApp : Application() { return builtInDisallowedPackageNames + userDisallowed } + fun getAppScopedViewModel(): VpnViewModel { + return vpnViewModel + } + val builtInDisallowedPackageNames: List = listOf( // RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 193e2ff..53579cc 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -10,33 +10,40 @@ import android.os.Build import android.system.OsConstants import android.util.Log import com.tailscale.ipn.mdm.MDMSettings +import com.tailscale.ipn.ui.model.Ipn +import com.tailscale.ipn.ui.notifier.Notifier import libtailscale.Libtailscale import java.util.UUID open class IPNService : VpnService(), libtailscale.IPNService { private val TAG = "IPNService" private val randomID: String = UUID.randomUUID().toString() + private lateinit var app: App override fun id(): String { return randomID } + override fun updateVpnStatus(status: Boolean) { + app.getAppScopedViewModel().setVpnActive(status) + } + override fun onCreate() { super.onCreate() // grab app to make sure it initializes - App.get() + app = App.get() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = when (intent?.action) { ACTION_STOP_VPN -> { - App.get().setWantRunning(false) + app.setWantRunning(false) close() START_NOT_STICKY } ACTION_START_VPN -> { showForegroundNotification() - App.get().setWantRunning(true) + app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY } @@ -44,8 +51,8 @@ open class IPNService : VpnService(), libtailscale.IPNService { // This means we were started by Android due to Always On VPN. // We show a non-foreground notification because we weren't // started as a foreground service. - App.get().notifyStatus(true) - App.get().setWantRunning(true) + app.notifyStatus(true) + app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY } @@ -64,6 +71,8 @@ open class IPNService : VpnService(), libtailscale.IPNService { } override fun close() { + app.setWantRunning(false) { updateVpnStatus(false) } + Notifier.setState(Ipn.State.Stopping) stopForeground(STOP_FOREGROUND_REMOVE) Libtailscale.serviceDisconnect(this) } @@ -78,6 +87,10 @@ open class IPNService : VpnService(), libtailscale.IPNService { super.onRevoke() } + private fun setVpnPrepared(isPrepared: Boolean) { + app.getAppScopedViewModel().setVpnPrepared(isPrepared) + } + private fun showForegroundNotification() { try { startForeground( diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 336edd9..bfdfd19 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -89,7 +89,7 @@ class MainActivity : ComponentActivity() { private lateinit var vpnPermissionLauncher: ActivityResultLauncher private val viewModel: MainViewModel by lazy { val app = App.get() - vpnViewModel = app.vpnViewModel + vpnViewModel = app.getAppScopedViewModel() ViewModelProvider(this, MainViewModelFactory(vpnViewModel)).get(MainViewModel::class.java) } private lateinit var vpnViewModel: VpnViewModel @@ -137,7 +137,7 @@ class MainActivity : ComponentActivity() { showOtherVPNConflictDialog() } else { Log.d("VpnPermission", "Permission was denied by the user") - viewModel.setVpnPrepared(false) + vpnViewModel.setVpnPrepared(false) } } } diff --git a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt new file mode 100644 index 0000000..622df91 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt @@ -0,0 +1,58 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package com.tailscale.ipn + +import android.content.Context +import android.net.ConnectivityManager +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.util.Log +import libtailscale.Libtailscale +import java.net.InetAddress +import java.net.NetworkInterface + +object NetworkChangeCallback { + + // requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is + // possible that this might return an unusuable network, eg a captive portal. + fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) { + val networkConnectivityRequest = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build() + + connectivityManager.registerNetworkCallback( + networkConnectivityRequest, + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + + val sb = StringBuilder() + val linkProperties: LinkProperties? = connectivityManager.getLinkProperties(network) + val dnsList: MutableList = linkProperties?.dnsServers ?: mutableListOf() + for (ip in dnsList) { + sb.append(ip.hostAddress).append(" ") + } + val searchDomains: String? = linkProperties?.domains + if (searchDomains != null) { + sb.append("\n") + sb.append(searchDomains) + } + + if (dns.updateDNSFromNetwork(sb.toString())) { + Libtailscale.onDNSConfigChanged(linkProperties?.interfaceName) + } + } + + override fun onLost(network: Network) { + super.onLost(network) + if (dns.updateDNSFromNetwork("")) { + Libtailscale.onDNSConfigChanged("") + } + } + }) + } +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt index 9fceead..b425d14 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt @@ -18,7 +18,11 @@ class Ipn { NeedsMachineAuth(3), Stopped(4), Starting(5), - Running(6); + Running(6), + // Stopping represents a state where a request to stop Tailscale has been issue but has not + // completed. This state allows UI to optimistically reflect a stopped state, and to fallback if + // necessary. + Stopping(7); companion object { fun fromInt(value: Int): State { diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt index c01a142..427397a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt @@ -33,7 +33,8 @@ object Notifier { private val decoder = Json { ignoreUnknownKeys = true } // General IPN Bus State - val state: StateFlow = MutableStateFlow(Ipn.State.NoState) + private val _state = MutableStateFlow(Ipn.State.NoState) + val state: StateFlow = _state val netmap: StateFlow = MutableStateFlow(null) val prefs: StateFlow = MutableStateFlow(null) val engineStatus: StateFlow = MutableStateFlow(null) @@ -68,22 +69,22 @@ object Notifier { NotifyWatchOpt.Prefs.value or NotifyWatchOpt.InitialState.value or NotifyWatchOpt.InitialHealthState.value - manager = - app.watchNotifications(mask.toLong()) { notification -> - val notify = decoder.decodeFromStream(notification.inputStream()) - notify.State?.let { state.set(Ipn.State.fromInt(it)) } - notify.NetMap?.let(netmap::set) - notify.Prefs?.let(prefs::set) - notify.Engine?.let(engineStatus::set) - notify.TailFSShares?.let(tailFSShares::set) - notify.BrowseToURL?.let(browseToURL::set) - notify.LoginFinished?.let { loginFinished.set(it.property) } - notify.Version?.let(version::set) - notify.OutgoingFiles?.let(outgoingFiles::set) - notify.FilesWaiting?.let(filesWaiting::set) - notify.IncomingFiles?.let(incomingFiles::set) - notify.Health?.let(health::set) - } + manager = + app.watchNotifications(mask.toLong()) { notification -> + val notify = decoder.decodeFromStream(notification.inputStream()) + notify.State?.let { state.set(Ipn.State.fromInt(it)) } + notify.NetMap?.let(netmap::set) + notify.Prefs?.let(prefs::set) + notify.Engine?.let(engineStatus::set) + notify.TailFSShares?.let(tailFSShares::set) + notify.BrowseToURL?.let(browseToURL::set) + notify.LoginFinished?.let { loginFinished.set(it.property) } + notify.Version?.let(version::set) + notify.OutgoingFiles?.let(outgoingFiles::set) + notify.FilesWaiting?.let(filesWaiting::set) + notify.IncomingFiles?.let(incomingFiles::set) + notify.Health?.let(health::set) + } } } @@ -107,4 +108,8 @@ object Notifier { InitialOutgoingFiles(64), InitialHealthState(128), } + + fun setState(newState: Ipn.State) { + _state.value = newState +} } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index c12b1b1..8dfb3f4 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -232,6 +232,9 @@ fun MainView( ConnectView( state, isPrepared, + // If Tailscale is stopping, don't automatically restart; wait for user to take + // action (eg, if the user connected to another VPN). + state != Ipn.State.Stopping, user, { viewModel.toggleVpn() }, { viewModel.login() }, @@ -407,6 +410,7 @@ fun StartingView() { fun ConnectView( state: Ipn.State, isPrepared: Boolean, + shouldStartAutomatically: Boolean, user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit, @@ -415,7 +419,7 @@ fun ConnectView( showVPNPermissionLauncherIfUnauthorized: () -> Unit ) { LaunchedEffect(isPrepared) { - if (!isPrepared) { + if (!isPrepared && shouldStartAutomatically) { showVPNPermissionLauncherIfUnauthorized() } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt index c38b5c0..3baab13 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt @@ -134,11 +134,6 @@ open class IpnViewModel : ViewModel() { } // VPN Control - - fun setVpnPrepared(prepared: Boolean) { - _vpnPrepared.value = prepared - } - fun startVPN() { UninitializedApp.get().startVPN() } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index 3ce6a30..cdfaace 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -72,6 +72,8 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { val isVpnPrepared: StateFlow = vpnViewModel.vpnPrepared + val isVpnActive: StateFlow = vpnViewModel.vpnActive + // Icon displayed in the button to present the health view val healthIcon: StateFlow = MutableStateFlow(null) @@ -97,30 +99,32 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { viewModelScope.launch { var previousState: State? = null - combine(Notifier.state, isVpnPrepared) { state, prepared -> state to prepared } - .collect { (currentState, prepared) -> - stateRes.set(userStringRes(currentState, previousState, prepared)) + combine(Notifier.state, isVpnActive) { state, active -> state to active } + .collect { (currentState, active) -> + // Determine the correct state resource string + stateRes.set(userStringRes(currentState, previousState, active)) + // Determine if the VPN toggle should be on val isOn = when { - prepared && currentState == State.Running || currentState == State.Starting -> - true - previousState == State.NoState && currentState == State.Starting -> + active && (currentState == State.Running || currentState == State.Starting) -> true + previousState == State.NoState && currentState == State.Starting -> true else -> false } + // Update the VPN toggle state _vpnToggleState.value = isOn + + // Update the previous state previousState = currentState } } viewModelScope.launch { - searchTerm - .debounce(250L) - .collect { term -> - peers.set(peerCategorizer.groupedAndFilteredPeers(term)) - } + searchTerm.debounce(250L).collect { term -> + peers.set(peerCategorizer.groupedAndFilteredPeers(term)) + } } viewModelScope.launch { @@ -181,17 +185,17 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } } -private fun userStringRes(currentState: State?, previousState: State?, vpnPrepared: Boolean): Int { +private fun userStringRes(currentState: State?, previousState: State?, vpnActive: Boolean): Int { return when { previousState == State.NoState && currentState == State.Starting -> R.string.starting currentState == State.NoState -> R.string.placeholder currentState == State.InUseOtherUser -> R.string.placeholder currentState == State.NeedsLogin -> - if (vpnPrepared) R.string.please_login else R.string.connect_to_vpn + if (vpnActive) R.string.please_login else R.string.connect_to_vpn currentState == State.NeedsMachineAuth -> R.string.needs_machine_auth currentState == State.Stopped -> R.string.stopped currentState == State.Starting -> R.string.starting - currentState == State.Running -> if (vpnPrepared) R.string.connected else R.string.placeholder + currentState == State.Running -> if (vpnActive) R.string.connected else R.string.placeholder else -> R.string.placeholder } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt index f1e33bc..3c4d40e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt @@ -26,9 +26,12 @@ class VpnViewModelFactory(private val application: Application) : ViewModelProvi // application scoped because Tailscale might be toggled on and off outside of the activity // lifecycle. class VpnViewModel(application: Application) : AndroidViewModel(application) { - // Whether the VPN is prepared + // Whether the VPN is prepared. This is set to true if the VPN application is already prepared, or if the user has previously consented to the VPN application. This is used to determine whether a VPN permission launcher needs to be shown. val _vpnPrepared = MutableStateFlow(false) val vpnPrepared: StateFlow = _vpnPrepared + // Whether a VPN interface has been established. This is set by net.updateTUN upon VpnServiceBuilder.establish, and consumed by UI to reflect VPN state. + val _vpnActive = MutableStateFlow(false) + val vpnActive: StateFlow = _vpnActive val TAG = "VpnViewModel" init { @@ -49,7 +52,11 @@ class VpnViewModel(application: Application) : AndroidViewModel(application) { } } - fun setVpnPrepared(prepared: Boolean) { - _vpnPrepared.value = prepared + fun setVpnActive(isActive: Boolean) { + _vpnActive.value = isActive + } + + fun setVpnPrepared(isPrepared: Boolean) { + _vpnPrepared.value = isPrepared } } diff --git a/go.mod b/go.mod index 59d0598..9c3291b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22.0 require ( github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 - golang.org/x/sys v0.22.0 inet.af/netaddr v0.0.0-20220617031823-097006376321 tailscale.com v1.73.0-pre.0.20240821174438-af3d3c433b67 ) @@ -87,6 +86,7 @@ require ( golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/libtailscale/backend.go b/libtailscale/backend.go index f108f10..908b7e2 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -150,7 +150,6 @@ func (a *App) runBackend(ctx context.Context) error { cfg configPair state ipn.State networkMap *netmap.NetworkMap - service IPNService ) stateCh := make(chan ipn.State) @@ -168,9 +167,9 @@ func (a *App) runBackend(ctx context.Context) error { select { case s := <-stateCh: state = s - if cfg.rcfg != nil && state >= ipn.Starting && service != nil { + if cfg.rcfg != nil && state >= ipn.Starting && vpnService.service != nil { // On state change, check if there are router or config changes requiring an update to VPNBuilder - if err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil { + if err := b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg); err != nil { if errors.Is(err, errMultipleUsers) { // TODO: surface error to user } @@ -193,13 +192,13 @@ func (a *App) runBackend(ctx context.Context) error { networkMap = n case c := <-configs: cfg = c - if b == nil || service == nil || cfg.rcfg == nil { + if b == nil || vpnService.service == nil || cfg.rcfg == nil { configErrs <- nil break } - configErrs <- b.updateTUN(service, cfg.rcfg, cfg.dcfg) + configErrs <- b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg) case s := <-onVPNRequested: - if service != nil && service.ID() == s.ID() { + if vpnService.service != nil && vpnService.service.ID() == s.ID() { // Still the same VPN instance, do nothing break } @@ -228,24 +227,24 @@ func (a *App) runBackend(ctx context.Context) error { // See https://github.com/tailscale/corp/issues/13814 b.backend.DebugRebind() - service = s + vpnService.service = s if networkMap != nil { // TODO } if cfg.rcfg != nil && state >= ipn.Starting { - if err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil { + if err := b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg); err != nil { log.Printf("VPN update failed: %v", err) - service.Close() + vpnService.service.Close() b.lastCfg = nil b.CloseTUNs() } } case s := <-onDisconnect: b.CloseTUNs() - if service != nil && service.ID() == s.ID() { + if vpnService.service != nil && vpnService.service.ID() == s.ID() { netns.SetAndroidProtectFunc(nil) - service = nil + vpnService.service = nil } case i := <-onDNSConfigChanged: if b != nil { diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index c6ebf51..f344baa 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -3,7 +3,9 @@ package libtailscale -import _ "golang.org/x/mobile/bind" +import ( + _ "golang.org/x/mobile/bind" +) // Start starts the application, storing state in the given dataDir and using // the given appCtx. @@ -73,6 +75,8 @@ type IPNService interface { NewBuilder() VPNServiceBuilder Close() + + UpdateVpnStatus(bool) } // VPNServiceBuilder corresponds to Android's VpnService.Builder. diff --git a/libtailscale/net.go b/libtailscale/net.go index ec3f60f..efa7e8f 100644 --- a/libtailscale/net.go +++ b/libtailscale/net.go @@ -12,9 +12,9 @@ import ( "reflect" "runtime/debug" "strings" + "syscall" "github.com/tailscale/wireguard-go/tun" - "golang.org/x/sys/unix" "inet.af/netaddr" "tailscale.com/net/dns" "tailscale.com/net/netmon" @@ -33,6 +33,15 @@ var errVPNNotPrepared = errors.New("VPN service not prepared or was revoked") // https://github.com/tailscale/tailscale/issues/2180 var errMultipleUsers = errors.New("VPN cannot be created on this device due to an Android bug with multiple users") +// VpnService contains the IPNService class from Android, the file descriptor, and whether the descriptor has been detached. +type VpnService struct { + service IPNService + fd int32 + fdDetached bool +} + +var vpnService = &VpnService{} + // Report interfaces in the device in net.Interface format. func (a *App) getInterfaces() ([]netmon.Interface, error) { var ifaces []netmon.Interface @@ -138,7 +147,8 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O if len(rcfg.LocalAddrs) == 0 { return nil } - builder := service.NewBuilder() + vpnService.service = service + builder := vpnService.service.NewBuilder() b.logger.Logf("updateTUN: got new builder") if err := builder.SetMTU(defaultMTU); err != nil { @@ -193,10 +203,15 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O parcelFD, err := builder.Establish() if err != nil { if strings.Contains(err.Error(), "INTERACT_ACROSS_USERS") { + // Update VPN status if VPN interface cannot be created + b.logger.Logf("updateTUN: could not establish VPN because %v", err) + vpnService.service.UpdateVpnStatus(false) return errMultipleUsers } return fmt.Errorf("VpnService.Builder.establish: %v", err) } + log.Printf("Setting vpn activity status to true") + vpnService.service.UpdateVpnStatus(true) b.logger.Logf("updateTUN: established VPN") if parcelFD == nil { @@ -205,6 +220,9 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O // detachFd. tunFD, err := parcelFD.Detach() + vpnService.fdDetached = true + vpnService.fd = tunFD + if err != nil { return fmt.Errorf("detachFd: %v", err) } @@ -213,7 +231,7 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O // Create TUN device. tunDev, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD)) if err != nil { - unix.Close(int(tunFD)) + closeFileDescriptor() return err } b.logger.Logf("updateTUN: created TUN device") @@ -226,10 +244,21 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O return nil } +func closeFileDescriptor() error { + if vpnService.fd != -1 && vpnService.fdDetached { + err := syscall.Close(int(vpnService.fd)) + vpnService.fd = -1 + vpnService.fdDetached = false + return fmt.Errorf("error closing file descriptor: %w", err) + } + return nil +} + // CloseVPN closes any active TUN devices. func (b *backend) CloseTUNs() { b.lastCfg = nil b.devices.Shutdown() + vpnService.service = nil } // ifname is the interface name retrieved from LinkProperties on network change. If a network is lost, an empty string is passed in. From eae87896281f2d4151438ff1b5171ef490076c44 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:45:54 -0700 Subject: [PATCH 03/91] android: move string into correct place (#481) Move MDM auth key strings into the MDM strings blcok Updates #cleanup Signed-off-by: kari-ts --- android/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index dfa41de..fb69053 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -208,6 +208,8 @@ Manage Tailnet lock visibility Shows or hides the UI to run the Android device as an exit node. Run as exit node visibility + Defines an auth key that will be used for login. + Auth Key Permissions @@ -298,7 +300,5 @@ Only one VPN can be active, and it appears another is already running. Before starting Tailscale, disable the other VPN. Go to Settings Cancel - Defines an auth key that will be used for login. - Auth Key From d94125e76700c65c52a5554f65d521a63ccd39ce Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:16:47 -0700 Subject: [PATCH 04/91] android: make settings button focusable and clickable (#484) Fixes tailscale/corp#22717 Signed-off-by: kari-ts --- .../com/tailscale/ipn/ui/view/MainView.kt | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 8dfb3f4..df19d78 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -187,22 +187,28 @@ fun MainView( } }, trailingContent = { - Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - when (user) { - null -> SettingsButton { navigation.onNavigateToSettings() } - else -> - Box( - contentAlignment = Alignment.Center, - modifier = - Modifier.size(42.dp).clip(CircleShape).clickable { - navigation.onNavigateToSettings() - }) { - Avatar(profile = user, size = 36) { - navigation.onNavigateToSettings() - } - } - } - } + Box( + modifier = + Modifier.weight(1f) + .focusable() + .clickable { navigation.onNavigateToSettings() } + .padding(8.dp), + contentAlignment = Alignment.CenterEnd) { + when (user) { + null -> SettingsButton { navigation.onNavigateToSettings() } + else -> + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(42.dp).clip(CircleShape).focusable().clickable { + navigation.onNavigateToSettings() + }) { + Avatar(profile = user, size = 36) { + navigation.onNavigateToSettings() + } + } + } + } }) when (state) { From b4ca226eb78bd7dc55b85ecc3bdd53ca8d0cf3c1 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:37:10 -0700 Subject: [PATCH 05/91] android: make clipboard values clickable and focusable (#483) also, use Column isntead of LazyColumn since the Tailnet lock view is a short list and doesn't require lazy rendering Fixes tailscale/corp#21737 Signed-off-by: kari-ts --- .../ipn/ui/util/ClipboardValueView.kt | 11 +- .../ipn/ui/view/TailnetLockSetupView.kt | 139 +++++++++--------- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt index 79a5c34..90e7b79 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt @@ -4,6 +4,7 @@ package com.tailscale.ipn.ui.util import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -20,17 +21,15 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import com.tailscale.ipn.R import com.tailscale.ipn.ui.theme.titledListItem -import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV @Composable fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) { val localClipboardManager = LocalClipboardManager.current val modifier = - if (isAndroidTV()) { - Modifier - } else { - Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) } - } + Modifier.focusable() + .clickable { + localClipboardManager.setText(AnnotatedString(value)) + } ListItem( colors = MaterialTheme.colorScheme.titledListItem, diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index a4ff740..b1a4074 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -42,89 +42,96 @@ fun TailnetLockSetupView( backToSettings: BackNavigation, model: TailnetLockSetupViewModel = viewModel(factory = TailnetLockSetupViewModelFactory()) ) { - val statusItems by model.statusItems.collectAsState() - val nodeKey by model.nodeKey.collectAsState() - val tailnetLockKey by model.tailnetLockKey.collectAsState() - val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub") - - Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> - LoadingIndicator.Wrap { - LazyColumn(modifier = Modifier.padding(innerPadding)) { - item(key = "header") { ExplainerView() } - - items(items = statusItems, key = { "status_${it.title}" }) { statusItem -> - Lists.ItemDivider() - - ListItem( - leadingContent = { - Icon( - painter = painterResource(id = statusItem.icon), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant) - }, - headlineContent = { Text(stringResource(statusItem.title)) }) - } - - item(key = "nodeKey") { - Lists.SectionDivider() - - ClipboardValueView( - value = nodeKey, - title = stringResource(R.string.node_key), - subtitle = stringResource(R.string.node_key_explainer)) - } - - item(key = "tailnetLockKey") { - Lists.SectionDivider() - - ClipboardValueView( - value = tailnetLockTlPubKey, - title = stringResource(R.string.tailnet_lock_key), - subtitle = stringResource(R.string.tailnet_lock_key_explainer)) + val statusItems by model.statusItems.collectAsState() + val nodeKey by model.nodeKey.collectAsState() + val tailnetLockKey by model.tailnetLockKey.collectAsState() + val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub") + + Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> + LoadingIndicator.Wrap { + Column( + modifier = Modifier + .padding(innerPadding) + .focusable() + .verticalScroll(rememberScrollState()) + .fillMaxSize() + ) { + ExplainerView() + + statusItems.forEach { statusItem -> + Lists.ItemDivider() + + ListItem( + leadingContent = { + Icon( + painter = painterResource(id = statusItem.icon), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + headlineContent = { Text(stringResource(statusItem.title)) } + ) + } + //Node key + Lists.SectionDivider() + ClipboardValueView( + value = nodeKey, + title = stringResource(R.string.node_key), + subtitle = stringResource(R.string.node_key_explainer) + ) + + // Tailnet lock key + Lists.SectionDivider() + ClipboardValueView( + value = tailnetLockTlPubKey, + title = stringResource(R.string.tailnet_lock_key), + subtitle = stringResource(R.string.tailnet_lock_key_explainer) + ) + } } - } } - } } @Composable private fun ExplainerView() { - val handler = LocalUriHandler.current - - Lists.MultilineDescription { - ClickableText( - explainerText(), - onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, - style = MaterialTheme.typography.bodyMedium) - } + val handler = LocalUriHandler.current + + Lists.MultilineDescription { + ClickableText( + explainerText(), + onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, + style = MaterialTheme.typography.bodyMedium + ) + } } @Composable fun explainerText(): AnnotatedString { - val annotatedString = buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) { - append(stringResource(id = R.string.tailnet_lock_explainer)) - } + val annotatedString = buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) { + append(stringResource(id = R.string.tailnet_lock_explainer)) + } - pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) + pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) - withStyle( - style = - SpanStyle( + withStyle( + style = SpanStyle( color = MaterialTheme.colorScheme.link, - textDecoration = TextDecoration.Underline)) { - append(stringResource(id = R.string.learn_more)) + textDecoration = TextDecoration.Underline + ) + ) { + append(stringResource(id = R.string.learn_more)) } - pop() - } - return annotatedString + pop() + } + return annotatedString } @Composable @Preview fun TailnetLockSetupViewPreview() { - val vm = TailnetLockSetupViewModel() - vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF") - vm.tailnetLockKey.set("C0FFEE-CAFE-50DA") - TailnetLockSetupView(backToSettings = {}, vm) + val vm = TailnetLockSetupViewModel() + vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF") + vm.tailnetLockKey.set("C0FFEE-CAFE-50DA") + TailnetLockSetupView(backToSettings = {}, vm) } From a9ff204ae49bc405f44ec4b898014910ae8a302a Mon Sep 17 00:00:00 2001 From: yin kaisheng Date: Fri, 30 Aug 2024 03:57:48 +0800 Subject: [PATCH 06/91] android: fix Hostname type in MaskedPrefs, it should be String type (#482) --- android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt index b425d14..5e1b35a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt @@ -162,7 +162,7 @@ class Ipn { ForceDaemonSet = true } - var Hostname: Boolean? = null + var Hostname: String? = null set(value) { field = value HostnameSet = true From 77eaadb360d7bd7468166bbe79ce978418af5186 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:58:18 -0700 Subject: [PATCH 07/91] android: fix missing imports (#486) android: make clipboard values clickable and focusable also, use Column isntead of LazyColumn since the Tailnet lock view is a short list and doesn't require lazy rendering Fixes tailscale/corp#21737 Signed-off-by: kari-ts Signed-off-by: kari-ts <135075563+kari-ts@users.noreply.github.com> --- .../ipn/ui/view/TailnetLockSetupView.kt | 87 +++++++++---------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index b1a4074..660c06f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -3,10 +3,13 @@ package com.tailscale.ipn.ui.view +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -42,53 +45,47 @@ fun TailnetLockSetupView( backToSettings: BackNavigation, model: TailnetLockSetupViewModel = viewModel(factory = TailnetLockSetupViewModelFactory()) ) { - val statusItems by model.statusItems.collectAsState() - val nodeKey by model.nodeKey.collectAsState() - val tailnetLockKey by model.tailnetLockKey.collectAsState() - val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub") + val statusItems by model.statusItems.collectAsState() + val nodeKey by model.nodeKey.collectAsState() + val tailnetLockKey by model.tailnetLockKey.collectAsState() + val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub") - Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> - LoadingIndicator.Wrap { - Column( - modifier = Modifier - .padding(innerPadding) - .focusable() - .verticalScroll(rememberScrollState()) - .fillMaxSize() - ) { - ExplainerView() + Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> + LoadingIndicator.Wrap { + Column( + modifier = + Modifier.padding(innerPadding) + .focusable() + .verticalScroll(rememberScrollState()) + .fillMaxSize()) { + ExplainerView() - statusItems.forEach { statusItem -> - Lists.ItemDivider() + statusItems.forEach { statusItem -> + Lists.ItemDivider() - ListItem( - leadingContent = { - Icon( - painter = painterResource(id = statusItem.icon), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - headlineContent = { Text(stringResource(statusItem.title)) } - ) - } - //Node key - Lists.SectionDivider() - ClipboardValueView( - value = nodeKey, - title = stringResource(R.string.node_key), - subtitle = stringResource(R.string.node_key_explainer) - ) - - // Tailnet lock key - Lists.SectionDivider() - ClipboardValueView( - value = tailnetLockTlPubKey, - title = stringResource(R.string.tailnet_lock_key), - subtitle = stringResource(R.string.tailnet_lock_key_explainer) - ) + ListItem( + leadingContent = { + Icon( + painter = painterResource(id = statusItem.icon), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant) + }, + headlineContent = { Text(stringResource(statusItem.title)) }) } - } + // Node key + Lists.SectionDivider() + ClipboardValueView( + value = nodeKey, + title = stringResource(R.string.node_key), + subtitle = stringResource(R.string.node_key_explainer)) + + // Tailnet lock key + Lists.SectionDivider() + ClipboardValueView( + value = tailnetLockTlPubKey, + title = stringResource(R.string.tailnet_lock_key), + subtitle = stringResource(R.string.tailnet_lock_key_explainer)) + } } } From 18e4b176c6b4966187b07ab61535a0a16adcaa72 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:14:17 -0700 Subject: [PATCH 08/91] android: fix missing '}' issue (#487) also run linter Updates #cleanup Signed-off-by: kari-ts --- .../ipn/ui/view/TailnetLockSetupView.kt | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index 660c06f..d21e6af 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -87,48 +87,46 @@ fun TailnetLockSetupView( subtitle = stringResource(R.string.tailnet_lock_key_explainer)) } } + } } @Composable private fun ExplainerView() { - val handler = LocalUriHandler.current + val handler = LocalUriHandler.current - Lists.MultilineDescription { - ClickableText( - explainerText(), - onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, - style = MaterialTheme.typography.bodyMedium - ) - } + Lists.MultilineDescription { + ClickableText( + explainerText(), + onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, + style = MaterialTheme.typography.bodyMedium) + } } @Composable fun explainerText(): AnnotatedString { - val annotatedString = buildAnnotatedString { - withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) { - append(stringResource(id = R.string.tailnet_lock_explainer)) - } + return buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) { + append(stringResource(id = R.string.tailnet_lock_explainer)) + } - pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) + pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) - withStyle( - style = SpanStyle( + withStyle( + style = + SpanStyle( color = MaterialTheme.colorScheme.link, - textDecoration = TextDecoration.Underline - ) - ) { - append(stringResource(id = R.string.learn_more)) + textDecoration = TextDecoration.Underline)) { + append(stringResource(id = R.string.learn_more)) } - pop() - } - return annotatedString + pop() + } } @Composable @Preview fun TailnetLockSetupViewPreview() { - val vm = TailnetLockSetupViewModel() - vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF") - vm.tailnetLockKey.set("C0FFEE-CAFE-50DA") - TailnetLockSetupView(backToSettings = {}, vm) + val vm = TailnetLockSetupViewModel() + vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF") + vm.tailnetLockKey.set("C0FFEE-CAFE-50DA") + TailnetLockSetupView(backToSettings = {}, vm) } From 19581721cfc0eaadaf09120a26c20bffcf5b43c4 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Thu, 29 Aug 2024 15:45:09 -0700 Subject: [PATCH 09/91] android: bump OSS to 1.73.73, use Go 1.23 (#485) Updates #cleanup OSS and Version updated to 1.73.73 Signed-off-by: Andrea Gottardo --- Makefile | 2 +- android/build.gradle | 2 +- docker/DockerFile.amd64-build | 2 +- docker/DockerFile.amd64-shell | 2 +- go.mod | 29 ++++++++++--------- go.sum | 54 +++++++++++++++++------------------ go.toolchain.rev | 2 +- 7 files changed, 46 insertions(+), 47 deletions(-) diff --git a/Makefile b/Makefile index d6d7185..4e104fb 100644 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ update-version: ## Update the version in build.gradle update-oss: ## Update the tailscale.com go module and update the version in build.gradle GOPROXY=direct go get tailscale.com@main go run tailscale.com/cmd/printdep --go > go.toolchain.rev - go mod tidy -compat=1.22 + go mod tidy -compat=1.23 # Get the commandline tools package, this provides (among other things) the sdkmanager binary. $(ANDROID_HOME)/cmdline-tools/latest/bin/sdkmanager: diff --git a/android/build.gradle b/android/build.gradle index f405d1e..78eba20 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.73.13-taf3d3c433-g536e1adcc42" + versionName "1.73.73-t959285e0c-ge437c2d917e" } compileOptions { diff --git a/docker/DockerFile.amd64-build b/docker/DockerFile.amd64-build index 09f51e5..4d1d475 100644 --- a/docker/DockerFile.amd64-build +++ b/docker/DockerFile.amd64-build @@ -24,7 +24,7 @@ ENV PATH $PATH:$HOME/bin:$ANDROID_HOME/platform-tools # We need some version of Go new enough to support the "embed" package # to run "go run tailscale.com/cmd/printdep" to figure out which Tailscale Go # version we need later, but otherwise this toolchain isn't used: -RUN curl -L https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /usr/local -zxv +RUN curl -L https://go.dev/dl/go1.23.0.linux-amd64.tar.gz | tar -C /usr/local -zxv RUN ln -s /usr/local/go/bin/go /usr/bin RUN mkdir -p $HOME/tailscale-android diff --git a/docker/DockerFile.amd64-shell b/docker/DockerFile.amd64-shell index bd67657..5f73272 100644 --- a/docker/DockerFile.amd64-shell +++ b/docker/DockerFile.amd64-shell @@ -24,7 +24,7 @@ ENV PATH $PATH:$HOME/bin:$ANDROID_HOME/platform-tools # We need some version of Go new enough to support the "embed" package # to run "go run tailscale.com/cmd/printdep" to figure out which Tailscale Go # version we need later, but otherwise this toolchain isn't used: -RUN curl -L https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /usr/local -zxv +RUN curl -L https://go.dev/dl/go1.23.0.linux-amd64.tar.gz | tar -C /usr/local -zxv RUN ln -s /usr/local/go/bin/go /usr/bin RUN mkdir -p $HOME/tailscale-android diff --git a/go.mod b/go.mod index 9c3291b..132633e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module github.com/tailscale/tailscale-android -go 1.22.0 +go 1.23 + +toolchain go1.23.0 require ( github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 - golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 + golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.73.0-pre.0.20240821174438-af3d3c433b67 + tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540 ) require ( @@ -46,7 +48,7 @@ require ( github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect - github.com/illarion/gonotify v1.0.1 // indirect + github.com/illarion/gonotify/v2 v2.0.2 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/jellydator/ttlcache/v3 v3.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -68,29 +70,28 @@ require ( github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect - github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect - github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/x448/float16 v0.8.4 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.23.0 // indirect + golang.org/x/tools v0.24.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect diff --git a/go.sum b/go.sum index 85869a0..13a5d11 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= -github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= -github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/illarion/gonotify/v2 v2.0.2 h1:oDH5yvxq9oiQGWUeut42uShcWzOy/hsT9E7pvO95+kQ= +github.com/illarion/gonotify/v2 v2.0.2/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= @@ -150,8 +150,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= -github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= -github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= @@ -166,8 +166,6 @@ github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ0 github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= -github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= @@ -189,27 +187,27 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1: golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 h1:dm00oNtDy265HReLTARPfIDXTRb2IG0jqQVpn7p5MKE= -golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87/go.mod h1:DN+F2TpepQEh5goqWnM3gopfFakSWM8OmHiz0rPRjT4= +golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= +golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab h1:KONOFF8Uy3b60HEzOsGnNghORNhY4ImyOx0PGm73K9k= +golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab/go.mod h1:udWezQGYjqrCxz5nV321pXQTx5oGbZx+khZvFjZNOPM= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -222,21 +220,21 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -258,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.73.0-pre.0.20240821174438-af3d3c433b67 h1:QiC96H1MvmzSjMn1Bs5oboovD+qXRecko3sJhbOfEr8= -tailscale.com v1.73.0-pre.0.20240821174438-af3d3c433b67/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= +tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540 h1:nike4kuThT/9ehqnIkGlx3zWeRyx1aJe3E7iUyed0ng= +tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540/go.mod h1:PdTJH9Hv0Bn2PguIZh1vfqiKmcRgp6MoE2PnxET1B3E= diff --git a/go.toolchain.rev b/go.toolchain.rev index 7d064e9..8ba67b5 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -22ef9eb38e9a2d21b4a45f7adc75addb05f3efb8 +32389dd21fef8fabc5c5f235346bf9248e79b412 From 095dae11954c350489c3e3bc98e3cb1164fdf142 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Tue, 3 Sep 2024 16:14:49 +0100 Subject: [PATCH 10/91] android: exclude MDM classes from ProGuard optimizations Updates tailscale/corp#22797 Signed-off-by: Anton Tolchanov --- android/proguard-rules.pro | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro index 0d39ac1..f95f0d7 100644 --- a/android/proguard-rules.pro +++ b/android/proguard-rules.pro @@ -3,6 +3,10 @@ native ; } +# Keep the classes with syspolicy MDM keys, some of which +# get used only by the Go backend. +-keep class com.tailscale.ipn.mdm.** { *; } + # Keep specific classes from Tink library -keep class com.google.crypto.tink.** { *; } @@ -18,4 +22,4 @@ # Keep Joda-Time classes -keep class org.joda.time.** { *; } --dontwarn org.joda.time.** \ No newline at end of file +-dontwarn org.joda.time.** From fb8a4f51dc720bdefa181e6de2f4848a9fedd827 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Tue, 3 Sep 2024 16:18:17 +0100 Subject: [PATCH 11/91] Makefile: fix docker-shell command line - Fix volume mounting (positional argument to `-v`) - Correct the make target name in README Updates tailscale/corp#19670 Signed-off-by: Anton Tolchanov --- Makefile | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4e104fb..7c835ee 100644 --- a/Makefile +++ b/Makefile @@ -265,7 +265,7 @@ docker-all: docker-build-image docker-run-build $(DOCKER_IMAGE) .PHONY: docker-shell docker-shell: ## Builds a docker image with the android build env and opens a shell docker build -f docker/DockerFile.amd64-shell -t tailscale-android-shell-amd64 . - docker run -v --rm $(CURDIR):/build/tailscale-android -it tailscale-android-shell-amd64 + docker run --rm -v $(CURDIR):/build/tailscale-android -it tailscale-android-shell-amd64 .PHONY: docker-remove-shell-image docker-remove-shell-image: ## Removes all docker shell image diff --git a/README.md b/README.md index ca3fd2b..e072061 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,13 @@ and XML files in Android Studio. Enable "Format on Save". If you wish to avoid installing software on your host system, a Docker based development strategy is available, you can build and start a shell with: ```sh -make dockershell +make docker-shell ``` Several other makefile recipes are available for setting up the proper build environment and running builds. -Note that the docker makefile recipes s will preserve the image and remove container on completion. -If changes are made to the build environment or toolchain, cached docker images may need to be rebuilt. +Note that the docker makefile recipes s will preserve the image and remove container on completion. +If changes are made to the build environment or toolchain, cached docker images may need to be rebuilt. The docker build image name is parameterized in the makefile and changing it provides a simple means to do this. ### Nix From ab7ab7373643f5d17700533a1351a89b2919b654 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Thu, 5 Sep 2024 14:52:46 -0400 Subject: [PATCH 12/91] android: fix versioning and bump oss (#490) * android: update docker image names for go 1.23 updates #cleanup We need to regenerate the docker images, we'll denote the new ones with a go1.23 extension. This also sets the TS_USE_TOOLCHAIN flag so we're using the corp toolchain which fixes some versioning script issues. Signed-off-by: Jonathan Nobels * android: bumping OSS OSS and Version updated to 1.73.104-te7b5e8c8c-g161457b99b5 Signed-off-by: Jonathan Nobels --------- Signed-off-by: Jonathan Nobels --- Makefile | 3 ++- android/build.gradle | 2 +- go.mod | 8 +++----- go.sum | 8 ++++---- version/tailscale-version.sh | 3 +++ 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 7c835ee..8e2697f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,8 @@ # The docker image to use for the build environment. Changing this # will force a rebuild of the docker image. If there is an existing image # with this name, it will be used. -DOCKER_IMAGE=tailscale-android-build-amd64 +DOCKER_IMAGE=tailscale-android-build-amd64-go1.23 +export TS_USE_TOOLCHAIN=1 DEBUG_APK=tailscale-debug.apk RELEASE_AAB=tailscale-release.aab diff --git a/android/build.gradle b/android/build.gradle index 78eba20..b8fe7b0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.73.73-t959285e0c-ge437c2d917e" + versionName "1.73.104-te7b5e8c8c-g161457b99b5" } compileOptions { diff --git a/go.mod b/go.mod index 132633e..ff089e8 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,12 @@ module github.com/tailscale/tailscale-android -go 1.23 - -toolchain go1.23.0 +go 1.23.0 require ( - github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 + github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540 + tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f ) require ( diff --git a/go.sum b/go.sum index 13a5d11..12385db 100644 --- a/go.sum +++ b/go.sum @@ -156,8 +156,8 @@ github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVL github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= -github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= -github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc= +github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540 h1:nike4kuThT/9ehqnIkGlx3zWeRyx1aJe3E7iUyed0ng= -tailscale.com v1.73.0-pre.0.20240829222721-959285e0c540/go.mod h1:PdTJH9Hv0Bn2PguIZh1vfqiKmcRgp6MoE2PnxET1B3E= +tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f h1:6nKO3Qt1da0I2XiK06I7cI4SYY4iSKxGHRFWoqB7YCA= +tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f/go.mod h1:8e0mb1njzhKGS7DXY1q0PhVGrlFjZYW+eNQkC6Mb2BE= diff --git a/version/tailscale-version.sh b/version/tailscale-version.sh index 16ce7ee..adc01dc 100755 --- a/version/tailscale-version.sh +++ b/version/tailscale-version.sh @@ -9,6 +9,9 @@ set -euo pipefail +# use the go toolchain from the tailscale.com +export TS_USE_TOOLCHAIN=1 + go_list=$(go list -m tailscale.com) # go list outputs `tailscale.com `. Extract the version. mod_version=${go_list#tailscale.com} From 9f87446ab68ca5f718d6eb58905d2951282d38a4 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Mon, 9 Sep 2024 16:17:02 -0400 Subject: [PATCH 13/91] android: bumping OSS to 1.73.114 (#492) OSS Updated to 1.73.114 Version 1.73.114-t0970615b1-gab7ab737364 updates #cleanup Signed-off-by: Jonathan Nobels --- android/build.gradle | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- go.toolchain.rev | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b8fe7b0..0befe7d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.73.104-te7b5e8c8c-g161457b99b5" + versionName "1.73.114-t0970615b1-gab7ab737364" } compileOptions { diff --git a/go.mod b/go.mod index ff089e8..4783945 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f + tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45 ) require ( @@ -46,7 +46,7 @@ require ( github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect - github.com/illarion/gonotify/v2 v2.0.2 // indirect + github.com/illarion/gonotify/v2 v2.0.3 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/jellydator/ttlcache/v3 v3.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 12385db..c944cfd 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= -github.com/illarion/gonotify/v2 v2.0.2 h1:oDH5yvxq9oiQGWUeut42uShcWzOy/hsT9E7pvO95+kQ= -github.com/illarion/gonotify/v2 v2.0.2/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= +github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= +github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f h1:6nKO3Qt1da0I2XiK06I7cI4SYY4iSKxGHRFWoqB7YCA= -tailscale.com v1.73.0-pre.0.20240905180038-e7b5e8c8cd9f/go.mod h1:8e0mb1njzhKGS7DXY1q0PhVGrlFjZYW+eNQkC6Mb2BE= +tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45 h1:3SgcY+JsyhIPYvFqgAWfPnQak2DHpFtsKrEMVJH1i+Q= +tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= diff --git a/go.toolchain.rev b/go.toolchain.rev index 8ba67b5..cc32040 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -32389dd21fef8fabc5c5f235346bf9248e79b412 +0a7392ba4471f578e5160b6ea21def6ae8e4a072 From 283e1ebcd8ad2671565fe8efdd1bcc3020f163e9 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:10:02 -0700 Subject: [PATCH 14/91] android: fix network callback race (#493) ConnectivityManager doesn't make guarantees about the order of network updates. Only use network updates for currently active network. Also, use registerDefaultNetworkCallback so that we are only listening for default networks. Updates tailscale/tailscale#13173 Signed-off-by: kari-ts --- .../tailscale/ipn/NetworkChangeCallback.kt | 97 +++++++++++++------ 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt index 622df91..9ae0194 100644 --- a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt +++ b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt @@ -2,48 +2,73 @@ // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn -import android.content.Context import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities -import android.net.NetworkRequest import android.util.Log import libtailscale.Libtailscale import java.net.InetAddress -import java.net.NetworkInterface object NetworkChangeCallback { - // requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is - // possible that this might return an unusuable network, eg a captive portal. + private const val TAG = "NetworkChangeCallback" + + // Cache LinkProperties and NetworkCapabilities since synchronous ConnectivityManager calls are + // prone to races. + // Since there is no guarantee for which update might come first, maybe update DNS configs on + // both. + val networkCapabilitiesCache = mutableMapOf() + val linkPropertiesCache = mutableMapOf() + + // requestDefaultNetworkCallback receives notifications about the default network. Listen for + // changes to the capabilities, which are guaranteed to come after a network becomes available per + // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network), + // in order to filter on non-VPN networks. fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) { - val networkConnectivityRequest = - NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .build() - - connectivityManager.registerNetworkCallback( - networkConnectivityRequest, + + connectivityManager.registerDefaultNetworkCallback( object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - super.onAvailable(network) - - val sb = StringBuilder() - val linkProperties: LinkProperties? = connectivityManager.getLinkProperties(network) - val dnsList: MutableList = linkProperties?.dnsServers ?: mutableListOf() - for (ip in dnsList) { - sb.append(ip.hostAddress).append(" ") - } - val searchDomains: String? = linkProperties?.domains - if (searchDomains != null) { - sb.append("\n") - sb.append(searchDomains) + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { + super.onCapabilitiesChanged(network, capabilities) + networkCapabilitiesCache[network] = capabilities + val linkProperties = linkPropertiesCache[network] + if (linkProperties != null && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + maybeUpdateDNSConfig(linkProperties, dns) + } else { + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) || + !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + Log.d( + TAG, + "Capabilities changed for network $network.toString(), but not updating DNS config because either this is a VPN network or non-internet network") + } else { + Log.d( + TAG, + "Capabilities changed for network $network.toString(), but not updating DNS config, because the LinkProperties hasn't been gotten yet") + } } + } - if (dns.updateDNSFromNetwork(sb.toString())) { - Libtailscale.onDNSConfigChanged(linkProperties?.interfaceName) + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + super.onLinkPropertiesChanged(network, linkProperties) + linkPropertiesCache[network] = linkProperties + val capabilities = networkCapabilitiesCache[network] + if (capabilities != null && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + maybeUpdateDNSConfig(linkProperties, dns) + } else { + if (capabilities == null) { + Log.d( + TAG, + "Capabilities changed for network $network.toString(), but not updating DNS config because capabilities haven't been gotten for this network yet") + } else { + Log.d( + TAG, + "Capabilities changed for network $network.toString(), but not updating DNS config, because this is a VPN network or non-Internet network") + } } } @@ -55,4 +80,20 @@ object NetworkChangeCallback { } }) } + + fun maybeUpdateDNSConfig(linkProperties: LinkProperties, dns: DnsConfig) { + val sb = StringBuilder() + val dnsList: MutableList = linkProperties.dnsServers ?: mutableListOf() + for (ip in dnsList) { + sb.append(ip.hostAddress).append(" ") + } + val searchDomains: String? = linkProperties.domains + if (searchDomains != null) { + sb.append("\n") + sb.append(searchDomains) + } + if (dns.updateDNSFromNetwork(sb.toString())) { + Libtailscale.onDNSConfigChanged(linkProperties.interfaceName) + } + } } From 45567146f4c38f3a8285ed05514a7c85ad586ef7 Mon Sep 17 00:00:00 2001 From: Andrew Dunham Date: Wed, 11 Sep 2024 12:21:06 -0400 Subject: [PATCH 15/91] android, libtailscale: pass BuildConfig to Go code; use for DNS config This commit wires up a method to allow the Tailscale Go backend to obtain the build configuration, and then adds a new build configuration to the build to control whether we fall back to the Google public DNS servers if we can't determine the platform's DNS configuration. This replaces the previous "IsPlayVersion" / "MaybeGoogle" check for whether to use the DNS servers as fallbacks, to allow users to decide this independently of what version of the Android app this is. Updates tailscale/tailscale#13431 Signed-off-by: Andrew Dunham --- android/build.gradle | 21 +++++++++++++------ .../src/main/java/com/tailscale/ipn/App.kt | 8 +++++++ libtailscale/backend.go | 13 ++++++++---- libtailscale/interfaces.go | 14 +++++++++++++ libtailscale/net.go | 2 +- libtailscale/tailscale.go | 5 +++++ 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 0befe7d..264223f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,6 +39,14 @@ android { targetSdkVersion 34 versionCode 241 versionName "1.73.114-t0970615b1-gab7ab737364" + + // This setting, which defaults to 'true', will cause Tailscale to fall + // back to the Google DNS servers if it cannot determine what the + // operating system's DNS configuration is. + // + // Set it to false either here or in your local.properties file to + // disable this behaviour. + buildConfigField "boolean", "USE_GOOGLE_DNS_FALLBACK", getLocalProperty("tailscale.useGoogleDnsFallback", "true") } compileOptions { @@ -54,6 +62,7 @@ android { jvmTarget = "17" } buildFeatures { + buildConfig true compose true } composeOptions { @@ -66,9 +75,9 @@ android { applicationTest { initWith debug manifestPlaceholders.leanbackRequired = false - buildConfigField "String", "GITHUB_USERNAME", "\"" + getLocalProperty("githubUsername")+"\"" - buildConfigField "String", "GITHUB_PASSWORD", "\"" + getLocalProperty("githubPassword")+"\"" - buildConfigField "String", "GITHUB_2FA_SECRET", "\"" + getLocalProperty("github2FASecret")+"\"" + buildConfigField "String", "GITHUB_USERNAME", "\"" + getLocalProperty("githubUsername", "")+"\"" + buildConfigField "String", "GITHUB_PASSWORD", "\"" + getLocalProperty("githubPassword", "")+"\"" + buildConfigField "String", "GITHUB_2FA_SECRET", "\"" + getLocalProperty("github2FASecret", "")+"\"" } debug { manifestPlaceholders.leanbackRequired = false @@ -156,12 +165,12 @@ dependencies { implementation("androidx.compose.ui:ui-tooling-preview") } -def getLocalProperty(key) { +def getLocalProperty(key, defaultValue) { try { Properties properties = new Properties() properties.load(project.file('local.properties').newDataInputStream()) - return properties.getProperty(key) + return properties.getProperty(key) ?: defaultValue } catch(Throwable ignored) { - return "" + return defaultValue } } diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 5df2cab..a9430dd 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import libtailscale.BuildConfig as GoBuildConfig import libtailscale.Libtailscale import java.io.File import java.io.IOException @@ -311,6 +312,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { } } + // getBuildConfig implements the libtailscale.AppContext interface. + override fun getBuildConfig(): GoBuildConfig { + var buildConfig = GoBuildConfig() + buildConfig.useGoogleDNSFallback = BuildConfig.USE_GOOGLE_DNS_FALLBACK + return buildConfig + } + fun notifyPolicyChanged() { app.notifyPolicyChanged() } diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 908b7e2..414eeeb 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -44,6 +44,9 @@ type App struct { // appCtx is a global reference to the com.tailscale.ipn.App instance. appCtx AppContext + // buildConfig is the build configuration for the app. + buildConfig *BuildConfig + store *stateStore policyStore *syspolicyHandler logIDPublicAtomic atomic.Pointer[logid.PublicID] @@ -97,7 +100,8 @@ type backend struct { // when no nameservers are provided by Tailscale. avoidEmptyDNS bool - appCtx AppContext + appCtx AppContext + buildConfig *BuildConfig } type settingsFunc func(*router.Config, *dns.OSConfig) error @@ -262,9 +266,10 @@ func (a *App) newBackend(dataDir, directFileRoot string, appCtx AppContext, stor logf := logger.RusagePrefixLog(log.Printf) b := &backend{ - devices: newTUNDevices(), - settings: settings, - appCtx: appCtx, + devices: newTUNDevices(), + settings: settings, + appCtx: appCtx, + buildConfig: a.buildConfig, } var logID logid.PrivateID logID.UnmarshalText([]byte("dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000")) diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index f344baa..11e40e2 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -57,6 +57,12 @@ type AppContext interface { // GetSyspolicyStringArrayValue returns the current string array value for the given system policy, // expressed as a JSON string. GetSyspolicyStringArrayJSONValue(key string) (string, error) + + // GetBuildConfig gets the build configuration of the Android app. + // + // The returned BuildConfig should not change during the lifetime of + // the app. + GetBuildConfig() *BuildConfig } // IPNService corresponds to our IPNService in Java. @@ -166,3 +172,11 @@ func RequestVPN(service IPNService) { func ServiceDisconnect(service IPNService) { onDisconnect <- service } + +// BuildConfig is a struct that represents the build configuration of the +// Android application, as set in BuildConfig.java. +type BuildConfig struct { + // UseGoogleDNSFallback is whether to fall back to the Google public + // DNS servers if the platform's DNS servers cannot be determined. + UseGoogleDNSFallback bool +} diff --git a/libtailscale/net.go b/libtailscale/net.go index efa7e8f..c49efd6 100644 --- a/libtailscale/net.go +++ b/libtailscale/net.go @@ -287,7 +287,7 @@ func (b *backend) getDNSBaseConfig() (ret dns.OSConfig, _ error) { // DNS config are lacking, and almost all Android phones use Google // services anyway, so it's a reasonable default: it's an ecosystem the // user has selected by having an Android device. - if len(ret.Nameservers) == 0 && b.appCtx.IsPlayVersion() { + if len(ret.Nameservers) == 0 && b.buildConfig.UseGoogleDNSFallback { log.Printf("getDNSBaseConfig: none found; falling back to Google public DNS") ret.Nameservers = append(ret.Nameservers, googleDNSServers...) } diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 7370726..5d08cd7 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -38,6 +38,11 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { } a.ready.Add(2) + // Get the build configuration, if any. + if bc := appCtx.GetBuildConfig(); bc != nil { + a.buildConfig = bc + } + a.store = newStateStore(a.appCtx) a.policyStore = &syspolicyHandler{a: a} netmon.RegisterInterfaceGetter(a.getInterfaces) From 28712da8d042c0a5d82072323dd59ac57275fb12 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:56:10 -0700 Subject: [PATCH 16/91] android: fix BuildConfig infinite loop (#495) Rather than create a Go struct that is set by Android, have Go call into Android to fetch build BuildConfig Updates tailscale/tailscale#13431 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/App.kt | 10 ++-------- libtailscale/backend.go | 13 ++++--------- libtailscale/interfaces.go | 17 +++-------------- libtailscale/net.go | 2 +- libtailscale/tailscale.go | 5 ----- 5 files changed, 10 insertions(+), 37 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index a9430dd..95acce5 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import libtailscale.BuildConfig as GoBuildConfig import libtailscale.Libtailscale import java.io.File import java.io.IOException @@ -80,6 +79,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override fun isPlayVersion(): Boolean = MaybeGoogle.isGoogle() + override fun shouldUseGoogleDNSFallback() : Boolean = BuildConfig.USE_GOOGLE_DNS_FALLBACK + override fun log(s: String, s1: String) { Log.d(s, s1) } @@ -312,13 +313,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { } } - // getBuildConfig implements the libtailscale.AppContext interface. - override fun getBuildConfig(): GoBuildConfig { - var buildConfig = GoBuildConfig() - buildConfig.useGoogleDNSFallback = BuildConfig.USE_GOOGLE_DNS_FALLBACK - return buildConfig - } - fun notifyPolicyChanged() { app.notifyPolicyChanged() } diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 414eeeb..908b7e2 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -44,9 +44,6 @@ type App struct { // appCtx is a global reference to the com.tailscale.ipn.App instance. appCtx AppContext - // buildConfig is the build configuration for the app. - buildConfig *BuildConfig - store *stateStore policyStore *syspolicyHandler logIDPublicAtomic atomic.Pointer[logid.PublicID] @@ -100,8 +97,7 @@ type backend struct { // when no nameservers are provided by Tailscale. avoidEmptyDNS bool - appCtx AppContext - buildConfig *BuildConfig + appCtx AppContext } type settingsFunc func(*router.Config, *dns.OSConfig) error @@ -266,10 +262,9 @@ func (a *App) newBackend(dataDir, directFileRoot string, appCtx AppContext, stor logf := logger.RusagePrefixLog(log.Printf) b := &backend{ - devices: newTUNDevices(), - settings: settings, - appCtx: appCtx, - buildConfig: a.buildConfig, + devices: newTUNDevices(), + settings: settings, + appCtx: appCtx, } var logID logid.PrivateID logID.UnmarshalText([]byte("dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000")) diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 11e40e2..d168fef 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -37,6 +37,9 @@ type AppContext interface { // (as opposed to F-droid/sideloaded). IsPlayVersion() bool + // ShouldUseGoogleDNSFallback reports whether or not to use Google for DNS fallback. + ShouldUseGoogleDNSFallback() bool + // IsChromeOS reports whether we're on a ChromeOS device. IsChromeOS() (bool, error) @@ -57,12 +60,6 @@ type AppContext interface { // GetSyspolicyStringArrayValue returns the current string array value for the given system policy, // expressed as a JSON string. GetSyspolicyStringArrayJSONValue(key string) (string, error) - - // GetBuildConfig gets the build configuration of the Android app. - // - // The returned BuildConfig should not change during the lifetime of - // the app. - GetBuildConfig() *BuildConfig } // IPNService corresponds to our IPNService in Java. @@ -172,11 +169,3 @@ func RequestVPN(service IPNService) { func ServiceDisconnect(service IPNService) { onDisconnect <- service } - -// BuildConfig is a struct that represents the build configuration of the -// Android application, as set in BuildConfig.java. -type BuildConfig struct { - // UseGoogleDNSFallback is whether to fall back to the Google public - // DNS servers if the platform's DNS servers cannot be determined. - UseGoogleDNSFallback bool -} diff --git a/libtailscale/net.go b/libtailscale/net.go index c49efd6..c4cb0a8 100644 --- a/libtailscale/net.go +++ b/libtailscale/net.go @@ -287,7 +287,7 @@ func (b *backend) getDNSBaseConfig() (ret dns.OSConfig, _ error) { // DNS config are lacking, and almost all Android phones use Google // services anyway, so it's a reasonable default: it's an ecosystem the // user has selected by having an Android device. - if len(ret.Nameservers) == 0 && b.buildConfig.UseGoogleDNSFallback { + if len(ret.Nameservers) == 0 && b.appCtx.ShouldUseGoogleDNSFallback() { log.Printf("getDNSBaseConfig: none found; falling back to Google public DNS") ret.Nameservers = append(ret.Nameservers, googleDNSServers...) } diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 5d08cd7..7370726 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -38,11 +38,6 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { } a.ready.Add(2) - // Get the build configuration, if any. - if bc := appCtx.GetBuildConfig(); bc != nil { - a.buildConfig = bc - } - a.store = newStateStore(a.appCtx) a.policyStore = &syspolicyHandler{a: a} netmon.RegisterInterfaceGetter(a.getInterfaces) From 33f79deb3a6900aa8348ec86e2b01904258c6eda Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 12 Sep 2024 12:01:02 -0700 Subject: [PATCH 17/91] tool/go: fix typo in comment Updates #cleanup Signed-off-by: Brad Fitzpatrick --- tool/go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool/go b/tool/go index 28734f0..4d6ed16 100755 --- a/tool/go +++ b/tool/go @@ -61,8 +61,8 @@ if [[ -z "${TOOLCHAINDIR}" ]]; then esac fi else - # fdroid supplies it's own toolchain, rather than using ours. + # fdroid supplies its own toolchain, rather than using ours. toolchain="${TOOLCHAINDIR}" fi -exec "${toolchain}/bin/go" "$@" \ No newline at end of file +exec "${toolchain}/bin/go" "$@" From aaecc62e1c0a19a705d25ff37b07f95897043bb2 Mon Sep 17 00:00:00 2001 From: Andrew Dunham Date: Thu, 12 Sep 2024 12:37:38 -0400 Subject: [PATCH 18/91] android: rework NetworkChangeCallback to track all networks Instead of just tracking our default network, track all of them and decide upon each change which is the "best" option. Updates tailscale/tailscale#13173 Signed-off-by: Andrew Dunham --- .../tailscale/ipn/NetworkChangeCallback.kt | 172 ++++++++++++------ 1 file changed, 120 insertions(+), 52 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt index 9ae0194..c3d57b6 100644 --- a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt +++ b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt @@ -6,94 +6,162 @@ import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.util.Log import libtailscale.Libtailscale import java.net.InetAddress +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock object NetworkChangeCallback { private const val TAG = "NetworkChangeCallback" - // Cache LinkProperties and NetworkCapabilities since synchronous ConnectivityManager calls are - // prone to races. - // Since there is no guarantee for which update might come first, maybe update DNS configs on - // both. - val networkCapabilitiesCache = mutableMapOf() - val linkPropertiesCache = mutableMapOf() - - // requestDefaultNetworkCallback receives notifications about the default network. Listen for - // changes to the capabilities, which are guaranteed to come after a network becomes available per - // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network), - // in order to filter on non-VPN networks. + private data class NetworkInfo( + var caps: NetworkCapabilities, + var linkProps: LinkProperties + ) + + private val lock = ReentrantLock() + private val activeNetworks = mutableMapOf() // keyed by Network + + // monitorDnsChanges sets up a network callback to monitor changes to the + // system's network state and update the DNS configuration when interfaces + // become available or properties of those interfaces change. fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) { + val networkConnectivityRequest = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build() - connectivityManager.registerDefaultNetworkCallback( + // Use registerNetworkCallback to listen for updates from all networks, and + // then update DNS configs for the best network. + // + // Note that we can't use registerDefaultNetworkCallback because the + // default network used by Tailscale will always show up with capability + // NOT_VPN=false, and we must filter out NOT_VPN networks to avoid routing + // loops. + connectivityManager.registerNetworkCallback( + networkConnectivityRequest, object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + + Log.d(TAG, "onAvailable: network ${network}") + lock.withLock { + activeNetworks[network] = NetworkInfo(NetworkCapabilities(), LinkProperties()) + maybeUpdateDNSConfigLocked("onAvailable", dns) + } + } + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { super.onCapabilitiesChanged(network, capabilities) - networkCapabilitiesCache[network] = capabilities - val linkProperties = linkPropertiesCache[network] - if (linkProperties != null && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { - maybeUpdateDNSConfig(linkProperties, dns) - } else { - if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) || - !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { - Log.d( - TAG, - "Capabilities changed for network $network.toString(), but not updating DNS config because either this is a VPN network or non-internet network") - } else { - Log.d( - TAG, - "Capabilities changed for network $network.toString(), but not updating DNS config, because the LinkProperties hasn't been gotten yet") - } + lock.withLock { + activeNetworks[network]?.caps = capabilities + maybeUpdateDNSConfigLocked("onCapabilitiesChanged", dns) } } override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { super.onLinkPropertiesChanged(network, linkProperties) - linkPropertiesCache[network] = linkProperties - val capabilities = networkCapabilitiesCache[network] - if (capabilities != null && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { - maybeUpdateDNSConfig(linkProperties, dns) - } else { - if (capabilities == null) { - Log.d( - TAG, - "Capabilities changed for network $network.toString(), but not updating DNS config because capabilities haven't been gotten for this network yet") - } else { - Log.d( - TAG, - "Capabilities changed for network $network.toString(), but not updating DNS config, because this is a VPN network or non-Internet network") - } + lock.withLock { + activeNetworks[network]?.linkProps = linkProperties + maybeUpdateDNSConfigLocked("onLinkPropertiesChanged", dns) } } override fun onLost(network: Network) { super.onLost(network) - if (dns.updateDNSFromNetwork("")) { - Libtailscale.onDNSConfigChanged("") + + Log.d(TAG, "onLost: network ${network}") + lock.withLock { + activeNetworks.remove(network) + maybeUpdateDNSConfigLocked("onLost", dns) } } }) } - fun maybeUpdateDNSConfig(linkProperties: LinkProperties, dns: DnsConfig) { + // pickNonMetered returns the first non-metered network in the list of + // networks, or the first network if none are non-metered. + private fun pickNonMetered(networks: Map): Network? { + for ((network, info) in networks) { + if (info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { + return network + } + } + return networks.keys.firstOrNull() + } + + // pickDefaultNetwork returns a non-VPN network to use as the 'default' + // network; one that is used as a gateway to the internet and from which we + // obtain our DNS servers. + private fun pickDefaultNetwork(): Network? { + // Filter the list of all networks to those that have the INTERNET + // capability, are not VPNs, and have a non-zero number of DNS servers + // available. + val networks = activeNetworks.filter { (_, info) -> + info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && + info.linkProps.dnsServers.isNotEmpty() == true + } + + // If we have one; just return it; otherwise, prefer networks that are also + // not metered (i.e. cell modems). + val nonMeteredNetwork = pickNonMetered(networks) + if (nonMeteredNetwork != null) { + return nonMeteredNetwork + } + + // Okay, less good; just return the first network that has the INTERNET and + // NOT_VPN capabilities; even though this interface doesn't have any DNS + // servers set, we'll use our DNS fallback servers to make queries. It's + // strictly better to return an interface + use the DNS fallback servers + // than to return nothing and not be able to route traffic. + for ((network, info) in activeNetworks) { + if (info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + Log.w(TAG, "no networks available that also have DNS servers set; falling back to first network ${network}") + return network + } + } + + // Otherwise, return nothing; we don't want to return a VPN network since + // it could result in a routing loop, and a non-INTERNET network isn't + // helpful. + Log.w(TAG, "no networks available to pick a default network") + return null + } + + // maybeUpdateDNSConfig will maybe update our DNS configuration based on the + // current set of active Networks. + // + // 'lock' must be held. + private fun maybeUpdateDNSConfigLocked(why: String, dns: DnsConfig) { + val defaultNetwork = pickDefaultNetwork() + if (defaultNetwork == null) { + Log.d(TAG, "${why}: no default network available; not updating DNS config") + return + } + val info = activeNetworks[defaultNetwork] + if (info == null) { + Log.w(TAG, "${why}: [unexpected] no info available for default network; not updating DNS config") + return + } + val sb = StringBuilder() - val dnsList: MutableList = linkProperties.dnsServers ?: mutableListOf() - for (ip in dnsList) { + for (ip in info.linkProps.dnsServers) { sb.append(ip.hostAddress).append(" ") } - val searchDomains: String? = linkProperties.domains + val searchDomains: String? = info.linkProps.domains if (searchDomains != null) { sb.append("\n") sb.append(searchDomains) } if (dns.updateDNSFromNetwork(sb.toString())) { - Libtailscale.onDNSConfigChanged(linkProperties.interfaceName) + Log.d(TAG, "${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})") + Libtailscale.onDNSConfigChanged(info.linkProps.interfaceName) } } } From e195def5e233f97426c3cbf7c125cba08124badc Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Thu, 12 Sep 2024 19:45:19 -0400 Subject: [PATCH 19/91] android: fix clean build (#499) updates tailscale/corp#17686 'make clean' will now purge the cached toolchain to ensure you're using the right go version when switching branches. make clean is run in CI before building anything and the docker container name is updated to pick that up. Signed-off-by: Jonathan Nobels --- Makefile | 10 ++++++++-- docker/DockerFile.amd64-build | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8e2697f..d18863d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,9 @@ # The docker image to use for the build environment. Changing this # will force a rebuild of the docker image. If there is an existing image # with this name, it will be used. -DOCKER_IMAGE=tailscale-android-build-amd64-go1.23 +# +# The convention here is tailscale-android-build-amd64- +DOCKER_IMAGE=tailscale-android-build-amd64-120924 export TS_USE_TOOLCHAIN=1 DEBUG_APK=tailscale-debug.apk @@ -274,7 +276,11 @@ docker-remove-shell-image: ## Removes all docker shell image .PHONY: clean clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. - -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab +clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. + @echo "Cleaning up old build artifacts" + -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab + @echo "Cleaning cached toolchain" + -rm -rf $(HOME)/.cache/tailscale-go -pkill -f gradle .PHONY: help diff --git a/docker/DockerFile.amd64-build b/docker/DockerFile.amd64-build index 4d1d475..c4fa38a 100644 --- a/docker/DockerFile.amd64-build +++ b/docker/DockerFile.amd64-build @@ -42,5 +42,5 @@ COPY android/gradle android/gradle RUN ./android/gradlew # Build the android app, bump the playstore version code, and make the tv release -CMD make release && make bump_version_code && make release-tv +CMD make clean && make release && make bump_version_code && make release-tv From ffbc556cde8653d88c0b9cfc4ac105d78f5aa0a1 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 13 Sep 2024 11:26:13 -0700 Subject: [PATCH 20/91] android: bumping OSS to 1.75.2 (#500) OSS and Version updated to 1.75.2-t93f61aa4c-ge195def5e23 Signed-off-by: Andrea Gottardo --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 264223f..50c530e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.73.114-t0970615b1-gab7ab737364" + versionName "1.75.2-t93f61aa4c-ge195def5e23" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 4783945..2babd34 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45 + tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c ) require ( diff --git a/go.sum b/go.sum index c944cfd..48e2a28 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45 h1:3SgcY+JsyhIPYvFqgAWfPnQak2DHpFtsKrEMVJH1i+Q= -tailscale.com v1.73.0-pre.0.20240909191529-0970615b1b45/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= +tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c h1:vfJj+AjWoyD9BpVY5q8js1iXxa43MUyAEHE/xgJEGnE= +tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= From 001e79546c2722a1447c395b10d59b131d5f5ac2 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 13 Sep 2024 12:59:15 -0700 Subject: [PATCH 21/91] android: bump OSS to 1.75.3 + update toolchain (#501) OSS and Version updated to 1.75.3-tafec2d41b-gffbc556cde8 Signed-off-by: Andrea Gottardo --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- go.toolchain.rev | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 50c530e..138c37f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.2-t93f61aa4c-ge195def5e23" + versionName "1.75.3-tafec2d41b-gffbc556cde8" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 2babd34..67078c8 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c + tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5 ) require ( diff --git a/go.sum b/go.sum index 48e2a28..5c6cfb8 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c h1:vfJj+AjWoyD9BpVY5q8js1iXxa43MUyAEHE/xgJEGnE= -tailscale.com v1.75.0-pre.0.20240912221814-93f61aa4cc2c/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= +tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5 h1:4ZBlVOSHigOjd93AUTv4WTVpIRlsq2cXkoxICrHHIg0= +tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= diff --git a/go.toolchain.rev b/go.toolchain.rev index cc32040..bf2677c 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -0a7392ba4471f578e5160b6ea21def6ae8e4a072 +4771bcafe5ca1554e13b2cb396336868e97781f3 From 72c410465cd475b02bce2e6e594f453af2782723 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 13 Sep 2024 15:16:43 -0700 Subject: [PATCH 22/91] Makefile: add command to start emulator This emulator command starts an emulator and keeps running in the foreground so as to avoid creating zombies. Updates #343 Co-authored-by: kari@tailscale.com Signed-off-by: James Tucker --- Makefile | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d18863d..f6c1103 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,19 @@ else export PATH := $(JAVA_HOME)/bin:$(PATH) endif +AVD_BASE_IMAGE := "system-images;android-33;google_apis;" +export HOST_ARCH=$(shell uname -m) +ifeq ($(HOST_ARCH),aarch64) + AVD_IMAGE := "$(AVD_BASE_IMAGE)arm64-v8a" +else ifeq ($(HOST_ARCH),arm64) + AVD_IMAGE := "$(AVD_BASE_IMAGE)arm64-v8a" +else + AVD_IMAGE := "$(AVD_BASE_IMAGE)x86_64" +endif +AVD ?= tailscale-$(HOST_ARCH) +export AVD_IMAGE +export AVD + # TOOLCHAINDIR is set by fdoid CI and used by tool/* scripts. TOOLCHAINDIR ?= export TOOLCHAINDIR @@ -160,6 +173,7 @@ env: @echo ANDROID_STUDIO_ROOT=$(ANDROID_STUDIO_ROOT) @echo JAVA_HOME=$(JAVA_HOME) @echo TOOLCHAINDIR=$(TOOLCHAINDIR) + @echo AVD_IMAGE="$(AVD_IMAGE)" # Ensure that JKS_PATH and JKS_PASSWORD are set before we attempt a build # that requires signing. @@ -238,6 +252,21 @@ checkandroidsdk: ## Check that Android SDK is installed test: gradle-dependencies ## Run the Android tests (cd android && ./gradlew test) +.PHONY: emulator +emulator: ## Start an android emulator instance + @echo "Checking installed SDK packages..." + @if ! $(ANDROID_HOME)/cmdline-tools/latest/bin/sdkmanager --list_installed | grep -q "$(AVD_IMAGE)"; then \ + echo "$(AVD_IMAGE) not found, installing..."; \ + $(ANDROID_HOME)/cmdline-tools/latest/bin/sdkmanager "$(AVD_IMAGE)"; \ + fi + @echo "Checking if AVD exists..." + @if ! $(ANDROID_HOME)/cmdline-tools/latest/bin/avdmanager list avd | grep -q "$(AVD)"; then \ + echo "AVD $(AVD) not found, creating..."; \ + $(ANDROID_HOME)/cmdline-tools/latest/bin/avdmanager create avd -n "$(AVD)" -k "$(AVD_IMAGE)"; \ + fi + @echo "Starting emulator..." + @$(ANDROID_HOME)/emulator/emulator -avd "$(AVD)" -logcat-output /dev/stdout -netdelay none -netspeed full + .PHONY: install install: $(DEBUG_APK) ## Install the debug APK on a connected device adb install -r $< @@ -278,7 +307,7 @@ docker-remove-shell-image: ## Removes all docker shell image clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. @echo "Cleaning up old build artifacts" - -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab + -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab @echo "Cleaning cached toolchain" -rm -rf $(HOME)/.cache/tailscale-go -pkill -f gradle From 7888447f3f33b8b148696f527de1c52f251259c4 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 13 Sep 2024 15:28:26 -0700 Subject: [PATCH 23/91] Makefile: disable GOTOOLCHAIN from dynamic switching Go has a new build facility that can utilize other toolchains if a module says so, but we manage the toolchain in our own way, so disable it. Updates #501 Signed-off-by: James Tucker --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index f6c1103..9ae8976 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,10 @@ AVD ?= tailscale-$(HOST_ARCH) export AVD_IMAGE export AVD +# Use our toolchain or the one that is specified, do not perform dynamic toolchain switching. +GOTOOLCHAIN=local +export GOTOOLCHAIN + # TOOLCHAINDIR is set by fdoid CI and used by tool/* scripts. TOOLCHAINDIR ?= export TOOLCHAINDIR From 209045d4f7e6c00580eac9bedcb692e1e52e8038 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 13 Sep 2024 15:35:50 -0700 Subject: [PATCH 24/91] Makefile: remove go toolchain version marker in clean as well The go wrapper script in tool/go assumes that .extracted is representative of the state of the toolchain directory, so it must be removed when the toolchain directory is removed. Updates #501 Signed-off-by: James Tucker --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9ae8976..852c1d2 100644 --- a/Makefile +++ b/Makefile @@ -313,7 +313,7 @@ clean: ## Remove build artifacts. Does not purge docker build envs. Use docker @echo "Cleaning up old build artifacts" -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab @echo "Cleaning cached toolchain" - -rm -rf $(HOME)/.cache/tailscale-go + -rm -rf $(HOME)/.cache/tailscale-go{,.extracted} -pkill -f gradle .PHONY: help From b3a7f7f2ae5f8966875d1b3a17086f38934ccec3 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 13 Sep 2024 15:56:27 -0700 Subject: [PATCH 25/91] tool/go: update to correct toolchain as needed Ensure that the target revision is loaded from this repository, not from the working directory. Update go to the target revision if the marker file does not match the target. Updates #501 Signed-off-by: James Tucker --- tool/go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tool/go b/tool/go index 4d6ed16..3d9bd3c 100755 --- a/tool/go +++ b/tool/go @@ -8,6 +8,8 @@ if [[ "${CI:-}" == "true" && "${NOBASHDEBUG:-}" != "true" ]]; then set -x fi +tsandroid=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd ) + # Allow TOOLCHAINDIR to be overridden, as a special case for the fdroid build if [[ -z "${TOOLCHAINDIR}" ]]; then toolchain="$HOME/.cache/tailscale-go" @@ -29,10 +31,13 @@ if [[ -z "${TOOLCHAINDIR}" ]]; then rm -rf "$toolchain" "$toolchain.extracted" fi fi - if [[ ! -d "$toolchain" ]]; then - mkdir -p "$HOME/.cache" - read -r REV Date: Fri, 13 Sep 2024 16:37:00 -0700 Subject: [PATCH 26/91] Makefile: use explicit path to command invocations Make does not respect PATH updates made inside the Makefile for program lookup in invocations. This can be worked around a number of ways, but differences between the Linux gmake versions in CI, and the macOS gmake versions on developer machines constrain us. Updates #501 Signed-off-by: James Tucker --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 852c1d2..73ae188 100644 --- a/Makefile +++ b/Makefile @@ -149,13 +149,13 @@ android/libs: mkdir -p android/libs $(GOBIN)/gomobile: $(GOBIN)/gobind go.mod go.sum - go install golang.org/x/mobile/cmd/gomobile + ./tool/go install golang.org/x/mobile/cmd/gomobile $(GOBIN)/gobind: go.mod go.sum - go install golang.org/x/mobile/cmd/gobind + ./tool/go install golang.org/x/mobile/cmd/gobind $(LIBTAILSCALE): Makefile android/libs $(shell find libtailscale -name *.go) go.mod go.sum $(GOBIN)/gomobile - gomobile bind -target android -androidapi 26 \ + $(GOBIN)/gomobile bind -target android -androidapi 26 \ -ldflags "$(FULL_LDFLAGS)" \ -o $@ ./libtailscale @@ -220,9 +220,9 @@ update-version: ## Update the version in build.gradle .PHONY: update-oss update-oss: ## Update the tailscale.com go module and update the version in build.gradle - GOPROXY=direct go get tailscale.com@main - go run tailscale.com/cmd/printdep --go > go.toolchain.rev - go mod tidy -compat=1.23 + GOPROXY=direct ./tool/go get tailscale.com@main + ./tool/go run tailscale.com/cmd/printdep --go > go.toolchain.rev + ./tool/go mod tidy -compat=1.23 # Get the commandline tools package, this provides (among other things) the sdkmanager binary. $(ANDROID_HOME)/cmdline-tools/latest/bin/sdkmanager: From 9e09fad0871b3b9856cfede81307400ddcf522f3 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Mon, 16 Sep 2024 13:06:45 -0700 Subject: [PATCH 27/91] Makefile: update go.toolchain.rev atomically Updates #501 Signed-off-by: James Tucker --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 73ae188..f8a0416 100644 --- a/Makefile +++ b/Makefile @@ -221,7 +221,8 @@ update-version: ## Update the version in build.gradle .PHONY: update-oss update-oss: ## Update the tailscale.com go module and update the version in build.gradle GOPROXY=direct ./tool/go get tailscale.com@main - ./tool/go run tailscale.com/cmd/printdep --go > go.toolchain.rev + ./tool/go run tailscale.com/cmd/printdep --go > go.toolchain.rev.new + mv go.toolchain.rev.new go.toolchain.rev ./tool/go mod tidy -compat=1.23 # Get the commandline tools package, this provides (among other things) the sdkmanager binary. From 2fcb080aa673ad14a8e1e07bac24ac985baff687 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Mon, 16 Sep 2024 13:11:50 -0700 Subject: [PATCH 28/91] Makefile: ensure go.toolchain.rev is included in bumposs Updates #501 Signed-off-by: James Tucker --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f8a0416..9373535 100644 --- a/Makefile +++ b/Makefile @@ -207,7 +207,7 @@ tag_release: ## Tag the current commit with the current version .PHONY: bumposs ## Bump to the latest oss and update teh versions. bumposs: update-oss update-version - git commit -sm "android: bumping OSS" -m "OSS and Version updated to ${VERSION_LONG}" android/build.gradle go.mod go.sum + git commit -sm "android: bumping OSS" -m "OSS and Version updated to ${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" .PHONY: bump_version_code From 8b91b0ff0aeda5c1bc96650d7cd0b20b3d7cd0ac Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:05:26 -0700 Subject: [PATCH 29/91] android: bumping OSS (#510) OSS and Version updated to 1.75.6-tf572286bf-g2fcb080aa67 Signed-off-by: kari-ts --- android/build.gradle | 2 +- go.mod | 4 ++-- go.sum | 4 ++-- go.toolchain.rev | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 138c37f..c7decd3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.3-tafec2d41b-gffbc556cde8" + versionName "1.75.6-tf572286bf-g2fcb080aa67" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 67078c8..36f044a 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/tailscale/tailscale-android -go 1.23.0 +go 1.23.1 require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5 + tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c ) require ( diff --git a/go.sum b/go.sum index 5c6cfb8..62971a8 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5 h1:4ZBlVOSHigOjd93AUTv4WTVpIRlsq2cXkoxICrHHIg0= -tailscale.com v1.75.0-pre.0.20240913175130-afec2d41b4f5/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= +tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c h1:USS40fVK7RcurquvNresLDlAPvOyGi9cZ+3CK7CfO+Q= +tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg= diff --git a/go.toolchain.rev b/go.toolchain.rev index bf2677c..cc32040 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -4771bcafe5ca1554e13b2cb396336868e97781f3 +0a7392ba4471f578e5160b6ea21def6ae8e4a072 From 9987dbc59270234252f84f768f18ff38338f222c Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:26:20 -0700 Subject: [PATCH 30/91] android: only update DNS configs on LinkProperties changes (#511) We were updating DNS configs when capabilities changed, without LinkProperties having been filled in. Because onAvailable always happened first, LinkProperties were created with default value, and onCapabilitiesChanged sent a DNS update using those LinkProperties. This change only updates DNS configs on LinkProperties, which is the last update sent on a network change. Updates tailscale/tailscale#13173 Signed-off-by: kari-ts --- .../tailscale/ipn/NetworkChangeCallback.kt | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt index c3d57b6..4dae7c1 100644 --- a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt +++ b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt @@ -9,7 +9,6 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.util.Log import libtailscale.Libtailscale -import java.net.InetAddress import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -17,12 +16,10 @@ object NetworkChangeCallback { private const val TAG = "NetworkChangeCallback" - private data class NetworkInfo( - var caps: NetworkCapabilities, - var linkProps: LinkProperties - ) + private data class NetworkInfo(var caps: NetworkCapabilities, var linkProps: LinkProperties) private val lock = ReentrantLock() + private val activeNetworks = mutableMapOf() // keyed by Network // monitorDnsChanges sets up a network callback to monitor changes to the @@ -30,13 +27,15 @@ object NetworkChangeCallback { // become available or properties of those interfaces change. fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) { val networkConnectivityRequest = - NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .build() + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build() // Use registerNetworkCallback to listen for updates from all networks, and - // then update DNS configs for the best network. + // then update DNS configs for the best network when LinkProperties are changed. + // Per + // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network), this happens after all other updates. // // Note that we can't use registerDefaultNetworkCallback because the // default network used by Tailscale will always show up with capability @@ -51,23 +50,19 @@ object NetworkChangeCallback { Log.d(TAG, "onAvailable: network ${network}") lock.withLock { activeNetworks[network] = NetworkInfo(NetworkCapabilities(), LinkProperties()) - maybeUpdateDNSConfigLocked("onAvailable", dns) } } override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { super.onCapabilitiesChanged(network, capabilities) - lock.withLock { - activeNetworks[network]?.caps = capabilities - maybeUpdateDNSConfigLocked("onCapabilitiesChanged", dns) - } + lock.withLock { activeNetworks[network]?.caps = capabilities } } override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { super.onLinkPropertiesChanged(network, linkProperties) lock.withLock { activeNetworks[network]?.linkProps = linkProperties - maybeUpdateDNSConfigLocked("onLinkPropertiesChanged", dns) + maybeUpdateDNSConfig("onLinkPropertiesChanged", dns) } } @@ -77,7 +72,7 @@ object NetworkChangeCallback { Log.d(TAG, "onLost: network ${network}") lock.withLock { activeNetworks.remove(network) - maybeUpdateDNSConfigLocked("onLost", dns) + maybeUpdateDNSConfig("onLost", dns) } } }) @@ -101,11 +96,12 @@ object NetworkChangeCallback { // Filter the list of all networks to those that have the INTERNET // capability, are not VPNs, and have a non-zero number of DNS servers // available. - val networks = activeNetworks.filter { (_, info) -> - info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && - info.linkProps.dnsServers.isNotEmpty() == true - } + val networks = + activeNetworks.filter { (_, info) -> + info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && + info.linkProps.dnsServers.isNotEmpty() == true + } // If we have one; just return it; otherwise, prefer networks that are also // not metered (i.e. cell modems). @@ -122,7 +118,9 @@ object NetworkChangeCallback { for ((network, info) in activeNetworks) { if (info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { - Log.w(TAG, "no networks available that also have DNS servers set; falling back to first network ${network}") + Log.w( + TAG, + "no networks available that also have DNS servers set; falling back to first network ${network}") return network } } @@ -136,9 +134,7 @@ object NetworkChangeCallback { // maybeUpdateDNSConfig will maybe update our DNS configuration based on the // current set of active Networks. - // - // 'lock' must be held. - private fun maybeUpdateDNSConfigLocked(why: String, dns: DnsConfig) { + private fun maybeUpdateDNSConfig(why: String, dns: DnsConfig) { val defaultNetwork = pickDefaultNetwork() if (defaultNetwork == null) { Log.d(TAG, "${why}: no default network available; not updating DNS config") @@ -146,7 +142,9 @@ object NetworkChangeCallback { } val info = activeNetworks[defaultNetwork] if (info == null) { - Log.w(TAG, "${why}: [unexpected] no info available for default network; not updating DNS config") + Log.w( + TAG, + "${why}: [unexpected] no info available for default network; not updating DNS config") return } @@ -160,7 +158,9 @@ object NetworkChangeCallback { sb.append(searchDomains) } if (dns.updateDNSFromNetwork(sb.toString())) { - Log.d(TAG, "${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})") + Log.d( + TAG, + "${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})") Libtailscale.onDNSConfigChanged(info.linkProps.interfaceName) } } From 0b2a04b4758cd6ef92fa962ac61b012bf680e456 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 17 Sep 2024 13:28:53 -0400 Subject: [PATCH 31/91] android: bump OSS to 1.75.11 (#512) android: bumping OSS OSS and Version updated to 1.75.11-t8b962f23d-gf07d419a125 The toolchain hash is being incorrectly by bumpOSS. Reverting it back to the correct value for 1.74/1.75 Signed-off-by: Jonathan Nobels --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- go.toolchain.rev | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index c7decd3..58b73eb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.6-tf572286bf-g2fcb080aa67" + versionName "1.75.11-t8b962f23d-gf07d419a125" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 36f044a..5f93650 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c + tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194 ) require ( diff --git a/go.sum b/go.sum index 62971a8..384903a 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c h1:USS40fVK7RcurquvNresLDlAPvOyGi9cZ+3CK7CfO+Q= -tailscale.com v1.75.0-pre.0.20240916200643-f572286bf92c/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg= +tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194 h1:WkfloEUujoWp7t5kxxktp2BhCaBewWAYHyNyz6WqkfQ= +tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg= diff --git a/go.toolchain.rev b/go.toolchain.rev index cc32040..bf2677c 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -0a7392ba4471f578e5160b6ea21def6ae8e4a072 +4771bcafe5ca1554e13b2cb396336868e97781f3 From fc8ccc005761230cd4ec9f658b3a56bd93496434 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:08:21 -0700 Subject: [PATCH 32/91] go/toolchain: use ed9dc37b2b000f376a3e819cbb159e2c17a2dac6 (#514) Updates #cleanup Signed-off-by: kari-ts --- go.toolchain.rev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.toolchain.rev b/go.toolchain.rev index bf2677c..22ed8ff 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -4771bcafe5ca1554e13b2cb396336868e97781f3 +ed9dc37b2b000f376a3e819cbb159e2c17a2dac6 \ No newline at end of file From 22de0cdb7eff8df25350d865d192a20c1587bd33 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:25:25 -0700 Subject: [PATCH 33/91] android: make custom url check case-insensitive (#513) Fixes tailscale/corp#23210 Signed-off-by: kari-ts Co-authored-by: Jonathan Nobels --- .../main/java/com/tailscale/ipn/ui/view/CustomLogin.kt | 8 ++++++-- .../tailscale/ipn/ui/viewModel/CustomLoginViewModel.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt index 9bbe5ab..82d2e37 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -25,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import com.tailscale.ipn.R import com.tailscale.ipn.ui.theme.listItem @@ -137,10 +139,12 @@ fun LoginView( onValueChange = { textVal = it }, placeholder = { Text(strings.placeholder, style = MaterialTheme.typography.bodySmall) - }) + }, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None) + ) }) - ListItem( + ListItem( colors = MaterialTheme.colorScheme.listItem, headlineContent = { Box(modifier = Modifier.fillMaxWidth()) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/CustomLoginViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/CustomLoginViewModel.kt index 0e830e6..ed2c581 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/CustomLoginViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/CustomLoginViewModel.kt @@ -36,7 +36,9 @@ class LoginWithCustomControlURLViewModel : CustomLoginViewModel() { // localAPIClient will use the default server if we give it a broken URL, // but we can make sure we can construct a URL from the input string and // ensure it has an http/https scheme - when (urlStr.startsWith("http") && urlStr.contains("://") && urlStr.length > 7) { + when (urlStr.startsWith("http", ignoreCase = true) && + urlStr.contains("://") && + urlStr.length > 7) { false -> { errorDialog.set(ErrorDialogType.INVALID_CUSTOM_URL) return From 2ec7304092ce5a8f29cb4e4fce7ea1709bf0d363 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:53:45 -0700 Subject: [PATCH 34/91] android: use onSuccess parameter in setWantRunning (#516) Previously we were never actually invoking this parameter We previously weren't setting vpnActive after closing IPNService Updates tailscale/corp#22284 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/App.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 95acce5..753200c 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -79,7 +79,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override fun isPlayVersion(): Boolean = MaybeGoogle.isGoogle() - override fun shouldUseGoogleDNSFallback() : Boolean = BuildConfig.USE_GOOGLE_DNS_FALLBACK + override fun shouldUseGoogleDNSFallback(): Boolean = BuildConfig.USE_GOOGLE_DNS_FALLBACK override fun log(s: String, s1: String) { Log.d(s, s1) @@ -160,7 +160,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { fun setWantRunning(wantRunning: Boolean, onSuccess: (() -> Unit)? = null) { val callback: (Result) -> Unit = { result -> result.fold( - onSuccess = {}, + onSuccess = { onSuccess?.invoke() }, onFailure = { error -> Log.d("TAG", "Set want running: failed to update preferences: ${error.message}") }) From 9654bb5d9dfb23b2104e76a830dca09a288b9d0c Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:37:50 -0700 Subject: [PATCH 35/91] android: include hex in LoginQRView (#502) Updates tailscale/tailscale#13277 Signed-off-by: kari-ts --- .../com/tailscale/ipn/ui/view/LoginQRView.kt | 8 ++++++++ .../ipn/ui/viewModel/LoginQRViewModel.kt | 18 +++++++++++++++++- android/src/main/res/values/strings.xml | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt index b3f465d..03ea17e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt @@ -40,6 +40,7 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( Surface(color = MaterialTheme.colorScheme.scrim, modifier = Modifier.fillMaxSize()) { Dialog(onDismissRequest = onDismiss) { val image by model.qrCode.collectAsState() + val numCode by model.numCode.collectAsState() Column( modifier = @@ -65,6 +66,12 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( modifier = Modifier.fillMaxSize()) } } + numCode?.let { it -> + Text( + text = stringResource(R.string.enter_code_to_connect_to_tailnet, it), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface) + } Button(onClick = onDismiss) { Text(text = stringResource(R.string.dismiss)) } } } @@ -76,5 +83,6 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( fun LoginQRViewPreview() { val vm = LoginQRViewModel() vm.qrCode.set(vm.generateQRCode("https://tailscale.com", 200, 0)) + vm.numCode.set("123456789") AppTheme { LoginQRView({}, vm) } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt index a3c5dcf..34d88df 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt @@ -20,12 +20,28 @@ import kotlinx.coroutines.launch class LoginQRViewModel : IpnViewModel() { + val numCode: StateFlow = MutableStateFlow(null) val qrCode: StateFlow = MutableStateFlow(null) init { viewModelScope.launch { Notifier.browseToURL.collect { url -> - url?.let { qrCode.set(generateQRCode(url, 200, 0)) } ?: run { qrCode.set(null) } + url?.let { + qrCode.set(generateQRCode(url, 200, 0)) + // Extract the string after "https://login.tailscale.com/a/" + val prefix = "https://login.tailscale.com/a/" + val code = + if (it.startsWith(prefix)) { + it.removePrefix(prefix) + } else { + null + } + numCode.set(code) + } + ?: run { + qrCode.set(null) + numCode.set(null) + } } } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index fb69053..da43495 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -244,6 +244,7 @@ Tailscale is a mesh VPN for securely connecting your devices. All connections are device-to-device, so we never see your data. We collect and use your email address and name, as well as your device name, OS version, and IP address in order to help you to connect your devices and manage your settings. We log when you are connected to your network. Scan this QR code to log in to your tailnet + or enter this code in the admin console: %1$s VPN is not ready to start From 9731afd44cb6580c1199d396a2b86f62413bf10b Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:59:12 -0700 Subject: [PATCH 36/91] android: use PackageManager to determine install AppSourceChecker (#517) We were using MaybeGoogle to determine whether the app was installed from the Play Store, but this has not worked since the refactor. Fixes tailscale/tailscale#13442 Updates tailscale/corp#23283 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 2 +- .../com/tailscale/ipn/AppSourceChecker.kt | 34 ++++++++++++++++++ .../java/com/tailscale/ipn/MaybeGoogle.java | 35 ------------------- libtailscale/backend.go | 4 +-- libtailscale/interfaces.go | 5 ++- 5 files changed, 38 insertions(+), 42 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt delete mode 100644 android/src/main/java/com/tailscale/ipn/MaybeGoogle.java diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 753200c..f0dd06d 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -77,7 +77,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override fun getPlatformDNSConfig(): String = dns.dnsConfigAsString - override fun isPlayVersion(): Boolean = MaybeGoogle.isGoogle() + override fun getInstallSource(): String = AppSourceChecker.getInstallSource(this) override fun shouldUseGoogleDNSFallback(): Boolean = BuildConfig.USE_GOOGLE_DNS_FALLBACK diff --git a/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt b/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt new file mode 100644 index 0000000..72e39fc --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt @@ -0,0 +1,34 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package com.tailscale.ipn + +import android.content.Context +import android.os.Build +import android.util.Log + +object AppSourceChecker { + + const val TAG = "AppSourceChecker" + + fun getInstallSource(context: Context): String { + val packageManager = context.packageManager + val packageName = context.packageName + Log.d(TAG, "Package name: $packageName") + + val installerPackageName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + packageManager.getInstallSourceInfo(packageName).installingPackageName + } else { + packageManager.getInstallerPackageName(packageName) + } + + Log.d(TAG, "Installer package name: $installerPackageName") + + return when (installerPackageName) { + "com.android.vending" -> "googleplay" + "org.fdroid.fdroid" -> "fdroid" + "com.amazon.venezia" -> "amazon" + null -> "unknown" + else -> "unknown($installerPackageName)" + } +} +} diff --git a/android/src/main/java/com/tailscale/ipn/MaybeGoogle.java b/android/src/main/java/com/tailscale/ipn/MaybeGoogle.java deleted file mode 100644 index 2eb3bac..0000000 --- a/android/src/main/java/com/tailscale/ipn/MaybeGoogle.java +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Activity; - -import java.lang.reflect.Method; - -public class MaybeGoogle { - static boolean isGoogle() { - return getGoogle() != null; - } - - static String getIdTokenForActivity(Activity act) { - Class google = getGoogle(); - if (google == null) { - return ""; - } - try { - Method method = google.getMethod("getIdTokenForActivity", Activity.class); - return (String) method.invoke(null, act); - } catch (Exception e) { - return ""; - } - } - - private static Class getGoogle() { - try { - return Class.forName("com.tailscale.ipn.Google"); - } catch (ClassNotFoundException e) { - return null; - } - } -} diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 908b7e2..247a802 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -105,9 +105,7 @@ type settingsFunc func(*router.Config, *dns.OSConfig) error func (a *App) runBackend(ctx context.Context) error { paths.AppSharedDir.Store(a.dataDir) hostinfo.SetOSVersion(a.osVersion()) - if !a.appCtx.IsPlayVersion() { - hostinfo.SetPackage("nogoogle") - } + hostinfo.SetPackage(a.appCtx.GetInstallSource()) deviceModel := a.modelName() if a.isChromeOS() { deviceModel = "ChromeOS: " + deviceModel diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index d168fef..7c05166 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -33,9 +33,8 @@ type AppContext interface { // GetModelName gets the Android device's model name. GetModelName() (string, error) - // IsPlayVersion reports whether this is the Google Play version of the app - // (as opposed to F-droid/sideloaded). - IsPlayVersion() bool + // GetInstallSource gets information about how the app was installed or updated. + GetInstallSource() string // ShouldUseGoogleDNSFallback reports whether or not to use Google for DNS fallback. ShouldUseGoogleDNSFallback() bool From f26a828cbd196f6fb87072cdc01b9006d60f4a35 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 20 Sep 2024 12:49:42 -0700 Subject: [PATCH 37/91] Makefile: use "tailscale_go" build tag when using Tailscale's Go toolchain Updates tailscale/tailscale#13527 Signed-off-by: Brad Fitzpatrick --- Makefile | 1 + build-tags.sh | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100755 build-tags.sh diff --git a/Makefile b/Makefile index 9373535..d6d3a41 100644 --- a/Makefile +++ b/Makefile @@ -156,6 +156,7 @@ $(GOBIN)/gobind: go.mod go.sum $(LIBTAILSCALE): Makefile android/libs $(shell find libtailscale -name *.go) go.mod go.sum $(GOBIN)/gomobile $(GOBIN)/gomobile bind -target android -androidapi 26 \ + -tags "$$(./build-tags.sh)" \ -ldflags "$(FULL_LDFLAGS)" \ -o $@ ./libtailscale diff --git a/build-tags.sh b/build-tags.sh new file mode 100755 index 0000000..de81fc9 --- /dev/null +++ b/build-tags.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [[ -z "$TOOLCHAIN_DIR" ]]; then + # By default, if TOOLCHAIN_DIR is unset, we assume we're + # using the Tailscale Go toolchain (github.com/tailscale/go) + # at the revision specified by go.toolchain.rev. If so, + # we tell our caller to use the "tailscale_go" build tag. + echo "tailscale_go" +else + # Otherwise, if TOOLCHAIN_DIR is specified, we assume + # we're F-Droid or something using a stock Go toolchain. + # That's fine. But we don't set the tailscale_go build tag. + # Return some no-op build tag that's non-empty for clarity + # when debugging. + echo "not_tailscale_go" +fi From 08ae01846890ba49702cee187cb1cb3fd0fbb5e0 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:25:15 -0700 Subject: [PATCH 38/91] android: send Android logs to logz (#515) TSLog sends log messages to Android's logcat and Tailscale's logger Libtailscale wrapper is a Kotlin wrapper that allows us to get around the problems with mocking a native library Fixes tailscale/corp#23191 Signed-off-by: kari-ts --- android/build.gradle | 3 + .../src/main/java/com/tailscale/ipn/App.kt | 27 ++-- .../main/java/com/tailscale/ipn/IPNService.kt | 10 +- .../java/com/tailscale/ipn/MainActivity.kt | 12 +- .../tailscale/ipn/NetworkChangeCallback.kt | 9 +- .../java/com/tailscale/ipn/ShareActivity.kt | 8 +- .../com/tailscale/ipn/StartVPNWorker.java | 4 +- .../com/tailscale/ipn/ui/localapi/Client.kt | 8 +- .../ipn/ui/notifier/HealthNotifier.kt | 18 +-- .../com/tailscale/ipn/ui/notifier/Notifier.kt | 5 +- .../com/tailscale/ipn/ui/util/TimeUtil.kt | 6 +- .../ipn/ui/viewModel/DNSSettingsViewModel.kt | 4 +- .../ipn/ui/viewModel/IpnViewModel.kt | 26 ++-- .../ipn/ui/viewModel/PingViewModel.kt | 8 +- .../ipn/ui/viewModel/TaildropViewModel.kt | 4 +- .../ipn/ui/viewModel/VpnViewModel.kt | 7 +- .../main/java/com/tailscale/ipn/util/TSLog.kt | 44 ++++++ .../com/tailcale/ipn/ui/util/TimeUtilTest.kt | 139 ++++++++++++------ libtailscale/callbacks.go | 3 + libtailscale/interfaces.go | 12 ++ libtailscale/tailscale.go | 9 ++ 21 files changed, 246 insertions(+), 120 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/util/TSLog.kt diff --git a/android/build.gradle b/android/build.gradle index 58b73eb..4d76c40 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -160,6 +160,9 @@ dependencies { // Unit Tests testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:5.4.0' + testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' debugImplementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.ui:ui-tooling-preview") diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index f0dd06d..dd89972 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -31,6 +31,7 @@ import com.tailscale.ipn.ui.notifier.HealthNotifier import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -162,7 +163,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { result.fold( onSuccess = { onSuccess?.invoke() }, onFailure = { error -> - Log.d("TAG", "Set want running: failed to update preferences: ${error.message}") + TSLog.d("TAG", "Set want running: failed to update preferences: ${error.message}") }) } Client(applicationScope) @@ -203,7 +204,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { private fun updateConnStatus(ableToStartVPN: Boolean) { setAbleToStartVPN(ableToStartVPN) QuickToggleService.updateTile() - Log.d("App", "Set Tile Ready: $ableToStartVPN") + TSLog.d("App", "Set Tile Ready: $ableToStartVPN") } override fun getModelName(): String { @@ -266,14 +267,14 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { downloads.mkdirs() } } catch (e: Exception) { - Log.e(TAG, "Failed to create downloads folder: $e") + TSLog.e(TAG, "Failed to create downloads folder: $e") downloads = File(this.filesDir, "Taildrop") try { if (!downloads.exists()) { downloads.mkdirs() } } catch (e: Exception) { - Log.e(TAG, "Failed to create Taildrop folder: $e") + TSLog.e(TAG, "Failed to create Taildrop folder: $e") downloads = File("") } } @@ -308,7 +309,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { val list = setting.value as? List<*> return Json.encodeToString(list) } catch (e: Exception) { - Log.d("MDM", "$key value cannot be serialized to JSON. Throwing NoSuchKeyException.") + TSLog.d("MDM", "$key value cannot be serialized to JSON. Throwing NoSuchKeyException.") throw MDMSettings.NoSuchKeyException() } } @@ -373,13 +374,13 @@ open class UninitializedApp : Application() { try { startForegroundService(intent) } catch (foregroundServiceStartException: IllegalStateException) { - Log.e( + TSLog.e( TAG, "startVPN hit ForegroundServiceStartNotAllowedException in startForegroundService(): $foregroundServiceStartException") } catch (securityException: SecurityException) { - Log.e(TAG, "startVPN hit SecurityException in startForegroundService(): $securityException") + TSLog.e(TAG, "startVPN hit SecurityException in startForegroundService(): $securityException") } catch (e: Exception) { - Log.e(TAG, "startVPN hit exception in startForegroundService(): $e") + TSLog.e(TAG, "startVPN hit exception in startForegroundService(): $e") } } @@ -388,9 +389,9 @@ open class UninitializedApp : Application() { try { startService(intent) } catch (illegalStateException: IllegalStateException) { - Log.e(TAG, "stopVPN hit IllegalStateException in startService(): $illegalStateException") + TSLog.e(TAG, "stopVPN hit IllegalStateException in startService(): $illegalStateException") } catch (e: Exception) { - Log.e(TAG, "stopVPN hit exception in startService(): $e") + TSLog.e(TAG, "stopVPN hit exception in startService(): $e") } } @@ -465,7 +466,7 @@ open class UninitializedApp : Application() { fun addUserDisallowedPackageName(packageName: String) { if (packageName.isEmpty()) { - Log.e(TAG, "addUserDisallowedPackageName called with empty packageName") + TSLog.e(TAG, "addUserDisallowedPackageName called with empty packageName") return } @@ -480,7 +481,7 @@ open class UninitializedApp : Application() { fun removeUserDisallowedPackageName(packageName: String) { if (packageName.isEmpty()) { - Log.e(TAG, "removeUserDisallowedPackageName called with empty packageName") + TSLog.e(TAG, "removeUserDisallowedPackageName called with empty packageName") return } @@ -498,7 +499,7 @@ open class UninitializedApp : Application() { val mdmDisallowed = MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList() if (mdmDisallowed.isNotEmpty()) { - Log.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed") + TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed") return builtInDisallowedPackageNames + mdmDisallowed } val userDisallowed = diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 53579cc..f7cc396 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -8,10 +8,10 @@ import android.content.pm.PackageManager import android.net.VpnService import android.os.Build import android.system.OsConstants -import android.util.Log import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier +import com.tailscale.ipn.util.TSLog import libtailscale.Libtailscale import java.util.UUID @@ -97,7 +97,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { UninitializedApp.STATUS_NOTIFICATION_ID, UninitializedApp.get().buildStatusNotification(true)) } catch (e: Exception) { - Log.e(TAG, "Failed to start foreground service: $e") + TSLog.e(TAG, "Failed to start foreground service: $e") } } @@ -113,7 +113,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { try { b.addDisallowedApplication(name) } catch (e: PackageManager.NameNotFoundException) { - Log.d(TAG, "Failed to add disallowed application: $e") + TSLog.d(TAG, "Failed to add disallowed application: $e") } } @@ -135,7 +135,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { // Tailscale, // then only allow those apps. for (packageName in includedPackages) { - Log.d(TAG, "Including app: $packageName") + TSLog.d(TAG, "Including app: $packageName") b.addAllowedApplication(packageName) } } else { @@ -143,7 +143,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { // - any app that the user manually disallowed in the GUI // - any app that we disallowed via hard-coding for (disallowedPackageName in UninitializedApp.get().disallowedPackageNames()) { - Log.d(TAG, "Disallowing app: $disallowedPackageName") + TSLog.d(TAG, "Disallowing app: $disallowedPackageName") disallowApp(b, disallowedPackageName) } } diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index bfdfd19..f3e4518 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -16,7 +16,6 @@ import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Bundle import android.provider.Settings -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher @@ -78,6 +77,7 @@ import com.tailscale.ipn.ui.viewModel.MainViewModelFactory import com.tailscale.ipn.ui.viewModel.PingViewModel import com.tailscale.ipn.ui.viewModel.SettingsNav import com.tailscale.ipn.ui.viewModel.VpnViewModel +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -128,15 +128,15 @@ class MainActivity : ComponentActivity() { vpnPermissionLauncher = registerForActivityResult(VpnPermissionContract()) { granted -> if (granted) { - Log.d("VpnPermission", "VPN permission granted") + TSLog.d("VpnPermission", "VPN permission granted") vpnViewModel.setVpnPrepared(true) App.get().startVPN() } else { if (isAnotherVpnActive(this)) { - Log.d("VpnPermission", "Another VPN is likely active") + TSLog.d("VpnPermission", "Another VPN is likely active") showOtherVPNConflictDialog() } else { - Log.d("VpnPermission", "Permission was denied by the user") + TSLog.d("VpnPermission", "Permission was denied by the user") vpnViewModel.setVpnPrepared(false) } } @@ -357,7 +357,7 @@ class MainActivity : ComponentActivity() { } } } catch (e: Exception) { - Log.e(TAG, "Login: failed to start MainActivity: $e") + TSLog.e(TAG, "Login: failed to start MainActivity: $e") } } @@ -371,7 +371,7 @@ class MainActivity : ComponentActivity() { val fallbackIntent = Intent(Intent.ACTION_VIEW, url) startActivity(fallbackIntent) } catch (e: Exception) { - Log.e(TAG, "Login: failed to open browser: $e") + TSLog.e(TAG, "Login: failed to open browser: $e") } } } diff --git a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt index 4dae7c1..9b6f9df 100644 --- a/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt +++ b/android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt @@ -8,6 +8,7 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.util.Log +import com.tailscale.ipn.util.TSLog import libtailscale.Libtailscale import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -47,7 +48,7 @@ object NetworkChangeCallback { override fun onAvailable(network: Network) { super.onAvailable(network) - Log.d(TAG, "onAvailable: network ${network}") + TSLog.d(TAG, "onAvailable: network ${network}") lock.withLock { activeNetworks[network] = NetworkInfo(NetworkCapabilities(), LinkProperties()) } @@ -69,7 +70,7 @@ object NetworkChangeCallback { override fun onLost(network: Network) { super.onLost(network) - Log.d(TAG, "onLost: network ${network}") + TSLog.d(TAG, "onLost: network ${network}") lock.withLock { activeNetworks.remove(network) maybeUpdateDNSConfig("onLost", dns) @@ -137,7 +138,7 @@ object NetworkChangeCallback { private fun maybeUpdateDNSConfig(why: String, dns: DnsConfig) { val defaultNetwork = pickDefaultNetwork() if (defaultNetwork == null) { - Log.d(TAG, "${why}: no default network available; not updating DNS config") + TSLog.d(TAG, "${why}: no default network available; not updating DNS config") return } val info = activeNetworks[defaultNetwork] @@ -158,7 +159,7 @@ object NetworkChangeCallback { sb.append(searchDomains) } if (dns.updateDNSFromNetwork(sb.toString())) { - Log.d( + TSLog.d( TAG, "${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})") Libtailscale.onDNSConfigChanged(info.linkProps.interfaceName) diff --git a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt index 899b9d3..efd3be7 100644 --- a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt @@ -8,7 +8,6 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.OpenableColumns -import android.util.Log import android.webkit.MimeTypeMap import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -20,6 +19,7 @@ import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.universalFit import com.tailscale.ipn.ui.view.TaildropView +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlin.random.Random @@ -59,7 +59,7 @@ class ShareActivity : ComponentActivity() { // Loads the files from the intent. fun loadFiles() { if (intent == null) { - Log.e(TAG, "Share failure - No intent found") + TSLog.e(TAG, "Share failure - No intent found") return } @@ -83,7 +83,7 @@ class ShareActivity : ComponentActivity() { } } else -> { - Log.e(TAG, "No extras found in intent - nothing to share") + TSLog.e(TAG, "No extras found in intent - nothing to share") null } } @@ -117,7 +117,7 @@ class ShareActivity : ComponentActivity() { } ?: emptyList() if (pendingFiles.isEmpty()) { - Log.e(TAG, "Share failure - no files extracted from intent") + TSLog.e(TAG, "Share failure - no files extracted from intent") } requestedTransfers.set(pendingFiles) diff --git a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java index d75e3fa..9ab4183 100644 --- a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java @@ -15,6 +15,8 @@ import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; +import com.tailscale.ipn.util.TSLog; + /** * A worker that exists to support IPNReceiver. */ @@ -38,7 +40,7 @@ public final class StartVPNWorker extends Worker { } // We aren't ready to start the VPN or don't have permission, open the Tailscale app. - android.util.Log.e("StartVPNWorker", "Tailscale isn't ready to start the VPN, notify the user."); + TSLog.e("StartVPNWorker", "Tailscale isn't ready to start the VPN, notify the user."); // Send notification NotificationManager notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt index 810a642..3ec004c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt @@ -4,7 +4,6 @@ package com.tailscale.ipn.ui.localapi import android.content.Context -import android.util.Log import com.tailscale.ipn.ui.model.BugReportID import com.tailscale.ipn.ui.model.Errors import com.tailscale.ipn.ui.model.Ipn @@ -13,6 +12,7 @@ import com.tailscale.ipn.ui.model.IpnState import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.util.InputStreamAdapter +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -175,7 +175,7 @@ class Client(private val scope: CoroutineScope) { }) } catch (e: Exception) { parts.forEach { it.body.close() } - Log.e(TAG, "Error creating file upload body: $e") + TSLog.e(TAG, "Error creating file upload body: $e") responseHandler(Result.failure(e)) return } @@ -307,7 +307,7 @@ class Request( @OptIn(ExperimentalSerializationApi::class) fun execute() { scope.launch(Dispatchers.IO) { - Log.d(TAG, "Executing request:${method}:${fullPath} on app $app") + TSLog.d(TAG, "Executing request:${method}:${fullPath} on app $app") try { val resp = if (parts != null) app.callLocalAPIMultipart(timeoutMillis, method, fullPath, parts) @@ -350,7 +350,7 @@ class Request( // The response handler will invoked internally by the request parser scope.launch { responseHandler(response) } } catch (e: Exception) { - Log.e(TAG, "Error executing request:${method}:${fullPath}: $e") + TSLog.e(TAG, "Error executing request:${method}:${fullPath}: $e") scope.launch { responseHandler(Result.failure(e)) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt index c9ed6db..5193328 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt @@ -5,7 +5,6 @@ package com.tailscale.ipn.ui.notifier import android.Manifest import android.content.pm.PackageManager -import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import com.tailscale.ipn.App @@ -14,6 +13,7 @@ import com.tailscale.ipn.UninitializedApp.Companion.notificationManager import com.tailscale.ipn.ui.model.Health import com.tailscale.ipn.ui.model.Health.UnhealthyState import com.tailscale.ipn.ui.util.set +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow @@ -47,7 +47,7 @@ class HealthNotifier( .distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() } .debounce(5000) .collect { health -> - Log.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") + TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") health?.Warnings?.let { notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray()) } @@ -76,22 +76,22 @@ class HealthNotifier( continue } else if (warning.hiddenByDependencies(currentWarnableCodes)) { // Ignore this warning because a dependency is also unhealthy - Log.d(TAG, "Ignoring ${warning.WarnableCode} because of dependency") + TSLog.d(TAG, "Ignoring ${warning.WarnableCode} because of dependency") continue } else if (!isWarmingUp) { - Log.d(TAG, "Adding health warning: ${warning.WarnableCode}") + TSLog.d(TAG, "Adding health warning: ${warning.WarnableCode}") this.currentWarnings.set(this.currentWarnings.value + warning) if (warning.Severity == Health.Severity.high) { this.sendNotification(warning.Title, warning.Text, warning.WarnableCode) } } else { - Log.d(TAG, "Ignoring ${warning.WarnableCode} because warming up") + TSLog.d(TAG, "Ignoring ${warning.WarnableCode} because warming up") } } val warningsToDrop = warningsBeforeAdd.minus(addedWarnings) if (warningsToDrop.isNotEmpty()) { - Log.d(TAG, "Dropping health warnings with codes $warningsToDrop") + TSLog.d(TAG, "Dropping health warnings with codes $warningsToDrop") this.removeNotifications(warningsToDrop) } currentWarnings.set(this.currentWarnings.value.subtract(warningsToDrop)) @@ -113,7 +113,7 @@ class HealthNotifier( } private fun sendNotification(title: String, text: String, code: String) { - Log.d(TAG, "Sending notification for $code") + TSLog.d(TAG, "Sending notification for $code") val notification = NotificationCompat.Builder(App.get().applicationContext, HEALTH_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) @@ -125,14 +125,14 @@ class HealthNotifier( if (ActivityCompat.checkSelfPermission( App.get().applicationContext, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Notification permission not granted") + TSLog.d(TAG, "Notification permission not granted") return } notificationManager.notify(code.hashCode(), notification) } private fun removeNotifications(warnings: Set) { - Log.d(TAG, "Removing notifications for $warnings") + TSLog.d(TAG, "Removing notifications for $warnings") for (warning in warnings) { notificationManager.cancel(warning.WarnableCode.hashCode()) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt index 427397a..faf78c8 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import com.tailscale.ipn.util.TSLog // Notifier is a wrapper around the IPN Bus notifier. It provides a way to watch // for changes in various parts of the Tailscale engine. You will typically only use @@ -59,7 +60,7 @@ object Notifier { @OptIn(ExperimentalSerializationApi::class) fun start(scope: CoroutineScope) { - Log.d(TAG, "Starting") + TSLog.d(TAG, "Starting Notifier") if (!::app.isInitialized) { App.get() } @@ -89,7 +90,7 @@ object Notifier { } fun stop() { - Log.d(TAG, "Stopping") + TSLog.d(TAG, "Stopping Notifier") manager?.let { it.stop() manager = null diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt b/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt index 970981f..43927be 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt @@ -3,8 +3,8 @@ package com.tailscale.ipn.ui.util -import android.util.Log import com.tailscale.ipn.R +import com.tailscale.ipn.util.TSLog import java.time.Duration import java.time.Instant import java.time.format.DateTimeFormatter @@ -108,12 +108,12 @@ object TimeUtil { 'm' -> durationFragment * 60.0 's' -> durationFragment else -> { - Log.e(TAG, "Invalid duration string: $goDuration") + TSLog.e(TAG, "Invalid duration string: $goDuration") return null } } } catch (e: NumberFormatException) { - Log.e(TAG, "Invalid duration string: $goDuration") + TSLog.e(TAG, "Invalid duration string: $goDuration") return null } valStr = "" diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt index 80a95cb..5ba489a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt @@ -3,7 +3,6 @@ package com.tailscale.ipn.ui.viewModel -import android.util.Log import androidx.annotation.StringRes import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -24,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import com.tailscale.ipn.util.TSLog class DNSSettingsViewModelFactory : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -43,7 +43,7 @@ class DNSSettingsViewModel : IpnViewModel() { .combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) } .stateIn(viewModelScope) .collect { (netmap, prefs) -> - Log.d("DNSSettingsViewModel", "prefs: CorpDNS=" + prefs?.CorpDNS.toString()) + TSLog.d("DNSSettingsViewModel", "prefs: CorpDNS=" + prefs?.CorpDNS.toString()) prefs?.let { if (it.CorpDNS) { enablementState.set(DNSEnablementState.ENABLED) diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt index 3baab13..20230f6 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt @@ -3,7 +3,6 @@ package com.tailscale.ipn.ui.viewModel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tailscale.ipn.UninitializedApp @@ -16,6 +15,7 @@ import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.set +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -130,7 +130,7 @@ open class IpnViewModel : ViewModel() { } .collect { nodeState -> _nodeState.value = nodeState } } - Log.d(TAG, "Created") + TSLog.d(TAG, "Created") } // VPN Control @@ -153,8 +153,8 @@ open class IpnViewModel : ViewModel() { val loginAction = { Client(viewModelScope).startLoginInteractive { result -> result - .onSuccess { Log.d(TAG, "Login started: $it") } - .onFailure { Log.e(TAG, "Error starting login: ${it.message}") } + .onSuccess { TSLog.d(TAG, "Login started: $it") } + .onFailure { TSLog.e(TAG, "Error starting login: ${it.message}") } completionHandler(result) } } @@ -165,7 +165,7 @@ open class IpnViewModel : ViewModel() { Client(viewModelScope).editPrefs(Ipn.MaskedPrefs().apply { WantRunning = false }) { result -> result .onSuccess { loginAction() } - .onFailure { Log.e(TAG, "Error setting wantRunning to false: ${it.message}") } + .onFailure { TSLog.e(TAG, "Error setting wantRunning to false: ${it.message}") } } } @@ -182,7 +182,7 @@ open class IpnViewModel : ViewModel() { if (mdmControlURL != null) { prefs = prefs ?: Ipn.MaskedPrefs() prefs.ControlURL = mdmControlURL - Log.d(TAG, "Overriding control URL with MDM value: $mdmControlURL") + TSLog.d(TAG, "Overriding control URL with MDM value: $mdmControlURL") } prefs?.let { @@ -210,8 +210,8 @@ open class IpnViewModel : ViewModel() { fun logout(completionHandler: (Result) -> Unit = {}) { Client(viewModelScope).logout { result -> result - .onSuccess { Log.d(TAG, "Logout started: $it") } - .onFailure { Log.e(TAG, "Error starting logout: ${it.message}") } + .onSuccess { TSLog.d(TAG, "Logout started: $it") } + .onFailure { TSLog.e(TAG, "Error starting logout: ${it.message}") } completionHandler(result) } } @@ -221,14 +221,14 @@ open class IpnViewModel : ViewModel() { private fun loadUserProfiles() { Client(viewModelScope).profiles { result -> result.onSuccess(loginProfiles::set).onFailure { - Log.e(TAG, "Error loading profiles: ${it.message}") + TSLog.e(TAG, "Error loading profiles: ${it.message}") } } Client(viewModelScope).currentProfile { result -> result .onSuccess { loggedInUser.set(if (it.isEmpty()) null else it) } - .onFailure { Log.e(TAG, "Error loading current profile: ${it.message}") } + .onFailure { TSLog.e(TAG, "Error loading current profile: ${it.message}") } } } @@ -242,7 +242,7 @@ open class IpnViewModel : ViewModel() { Client(viewModelScope).editPrefs(Ipn.MaskedPrefs().apply { WantRunning = false }) { result -> result .onSuccess { switchProfile() } - .onFailure { Log.e(TAG, "Error setting wantRunning to false: ${it.message}") } + .onFailure { TSLog.e(TAG, "Error setting wantRunning to false: ${it.message}") } } } @@ -277,7 +277,7 @@ open class IpnViewModel : ViewModel() { Client(viewModelScope).setUseExitNode(true) { LoadingIndicator.stop() } } else { // This should not be possible. In this state the button is hidden - Log.e(TAG, "No exit node to disable and no prior exit node to enable") + TSLog.e(TAG, "No exit node to disable and no prior exit node to enable") } } @@ -292,7 +292,7 @@ open class IpnViewModel : ViewModel() { } Client(viewModelScope).editPrefs(newPrefs) { result -> LoadingIndicator.stop() - Log.d("RunExitNodeViewModel", "Edited prefs: $result") + TSLog.d("RunExitNodeViewModel", "Edited prefs: $result") } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PingViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PingViewModel.kt index d0ad1f2..eedd3d0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PingViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PingViewModel.kt @@ -5,7 +5,6 @@ package com.tailscale.ipn.ui.viewModel import android.content.Context import android.os.CountDownTimer -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -16,6 +15,7 @@ import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.util.ConnectionMode import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.view.roundedString +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -39,7 +39,7 @@ class PingViewModel : ViewModel() { } override fun onFinish() { - Log.d(TAG, "Ping timer terminated") + TSLog.d(TAG, "Ping timer terminated") } } @@ -94,7 +94,7 @@ class PingViewModel : ViewModel() { response.onFailure { error -> val context: Context = App.get().applicationContext val stringError = error.toString() - Log.d(TAG, "Ping request failed: $stringError") + TSLog.d(TAG, "Ping request failed: $stringError") if (stringError.contains("timeout")) { this.errorMessage.set( context.getString( @@ -125,7 +125,7 @@ class PingViewModel : ViewModel() { } } } - statusResult.onFailure { Log.d(TAG, "Failed to fetch status: $it") } + statusResult.onFailure { TSLog.d(TAG, "Failed to fetch status: $it") } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/TaildropViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/TaildropViewModel.kt index 3d55057..92bce33 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/TaildropViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/TaildropViewModel.kt @@ -4,7 +4,6 @@ package com.tailscale.ipn.ui.viewModel import android.content.Context -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.MaterialTheme @@ -26,6 +25,7 @@ import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.view.ActivityIndicator import com.tailscale.ipn.ui.view.CheckedIndicator import com.tailscale.ipn.ui.view.ErrorDialogType +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -144,7 +144,7 @@ class TaildropViewModel( allSharablePeers.filter { !(it.Online ?: false) }.sortedBy { it.Name } myPeers.set(onlinePeers + offlinePeers) } - .onFailure { Log.e(TAG, "Error loading targets: ${it.message}") } + .onFailure { TSLog.e(TAG, "Error loading targets: ${it.message}") } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt index 3c4d40e..a6ee734 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/VpnViewModel.kt @@ -26,10 +26,13 @@ class VpnViewModelFactory(private val application: Application) : ViewModelProvi // application scoped because Tailscale might be toggled on and off outside of the activity // lifecycle. class VpnViewModel(application: Application) : AndroidViewModel(application) { - // Whether the VPN is prepared. This is set to true if the VPN application is already prepared, or if the user has previously consented to the VPN application. This is used to determine whether a VPN permission launcher needs to be shown. + // Whether the VPN is prepared. This is set to true if the VPN application is already prepared, or + // if the user has previously consented to the VPN application. This is used to determine whether + // a VPN permission launcher needs to be shown. val _vpnPrepared = MutableStateFlow(false) val vpnPrepared: StateFlow = _vpnPrepared - // Whether a VPN interface has been established. This is set by net.updateTUN upon VpnServiceBuilder.establish, and consumed by UI to reflect VPN state. + // Whether a VPN interface has been established. This is set by net.updateTUN upon + // VpnServiceBuilder.establish, and consumed by UI to reflect VPN state. val _vpnActive = MutableStateFlow(false) val vpnActive: StateFlow = _vpnActive val TAG = "VpnViewModel" diff --git a/android/src/main/java/com/tailscale/ipn/util/TSLog.kt b/android/src/main/java/com/tailscale/ipn/util/TSLog.kt new file mode 100644 index 0000000..4394574 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/util/TSLog.kt @@ -0,0 +1,44 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package com.tailscale.ipn.util + +import android.util.Log +import libtailscale.Libtailscale + +object TSLog { + var libtailscaleWrapper = LibtailscaleWrapper() + + fun d(tag: String?, message: String) { + Log.d(tag, message) + libtailscaleWrapper.sendLog(tag, message) + } + + fun w(tag: String, message: String) { + Log.w(tag, message) + libtailscaleWrapper.sendLog(tag, message) + } + + // Overloaded function without Throwable because Java does not support default parameters + @JvmStatic + fun e(tag: String?, message: String) { + Log.e(tag, message) + libtailscaleWrapper.sendLog(tag, message) + } + + fun e(tag: String?, message: String, throwable: Throwable? = null) { + if (throwable == null) { + Log.e(tag, message) + libtailscaleWrapper.sendLog(tag, message) + } else { + Log.e(tag, message, throwable) + libtailscaleWrapper.sendLog(tag, "$message ${throwable?.localizedMessage}") + } + } + + class LibtailscaleWrapper { + public fun sendLog(tag: String?, message: String) { + val logTag = tag ?: "" + Libtailscale.sendLog((logTag + ": " + message).toByteArray(Charsets.UTF_8)) + } + } +} diff --git a/android/src/test/kotlin/com/tailcale/ipn/ui/util/TimeUtilTest.kt b/android/src/test/kotlin/com/tailcale/ipn/ui/util/TimeUtilTest.kt index ddea99e..743e574 100644 --- a/android/src/test/kotlin/com/tailcale/ipn/ui/util/TimeUtilTest.kt +++ b/android/src/test/kotlin/com/tailcale/ipn/ui/util/TimeUtilTest.kt @@ -1,60 +1,107 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause + package com.tailcale.ipn.ui.util + import com.tailscale.ipn.ui.util.TimeUtil +import com.tailscale.ipn.util.TSLog +import com.tailscale.ipn.util.TSLog.LibtailscaleWrapper +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.mock import java.time.Duration + class TimeUtilTest { - @Test - fun durationInvalidMsUnits() { - val input = "5s10ms" - val actual = TimeUtil.duration(input) - assertNull("Should return null", actual) - } - - @Test - fun durationInvalidUsUnits() { - val input = "5s10us" - val actual = TimeUtil.duration(input) - assertNull("Should return null", actual) - } - - @Test - fun durationTestHappyPath() { - val input = arrayOf("1.0y1.0w1.0d1.0h1.0m1.0s", "1s", "1m", "1h", "1d", "1w", "1y") - val expectedSeconds = - arrayOf((31536000 + 604800 + 86400 + 3600 + 60 + 1), 1, 60, 3600, 86400, 604800, 31536000) - val expected = expectedSeconds.map { Duration.ofSeconds(it.toLong()) } - val actual = input.map { TimeUtil.duration(it) } - assertEquals("Incorrect conversion", expected, actual) - } - - @Test - fun testBadDurationString() { - val input = "1..0y1.0w1.0d1.0h1.0m1.0s" - val actual = TimeUtil.duration(input) - assertNull("Should return null", actual) - } - - @Test - fun testBadDInputString() { - val input = "1.0yy1.0w1.0d1.0h1.0m1.0s" - val actual = TimeUtil.duration(input) - assertNull("Should return null", actual) - } - - @Test - fun testIgnoreFractionalSeconds() { - val input = "10.9s" - val expectedSeconds = 10 - val expected = Duration.ofSeconds(expectedSeconds.toLong()) - val actual = TimeUtil.duration(input) - assertEquals("Should return $expectedSeconds seconds", expected, actual) - } + + private lateinit var libtailscaleWrapperMock: LibtailscaleWrapper + private lateinit var originalWrapper: LibtailscaleWrapper + + + @Before + fun setUp() { + libtailscaleWrapperMock = mock(LibtailscaleWrapper::class.java) + doNothing().`when`(libtailscaleWrapperMock).sendLog(anyString(), anyString()) + + + // Store the original wrapper so we can reset it later + originalWrapper = TSLog.libtailscaleWrapper + // Inject mock into TSLog + TSLog.libtailscaleWrapper = libtailscaleWrapperMock + } + + + @After + fun tearDown() { + // Reset TSLog after each test to avoid side effects + TSLog.libtailscaleWrapper = originalWrapper + } + + + @Test + fun durationInvalidMsUnits() { + val input = "5s10ms" + val actual = TimeUtil.duration(input) + assertNull("Should return null", actual) + } + + + @Test + fun durationInvalidUsUnits() { + val input = "5s10us" + val actual = TimeUtil.duration(input) + assertNull("Should return null", actual) + } + + + @Test + fun durationTestHappyPath() { + val input = arrayOf("1.0y1.0w1.0d1.0h1.0m1.0s", "1s", "1m", "1h", "1d", "1w", "1y") + val expectedSeconds = + arrayOf((31536000 + 604800 + 86400 + 3600 + 60 + 1), 1, 60, 3600, 86400, 604800, 31536000) + val expected = expectedSeconds.map { Duration.ofSeconds(it.toLong()) } + val actual = input.map { TimeUtil.duration(it) } + assertEquals("Incorrect conversion", expected, actual) + } + + + @Test + fun testBadDurationString() { + val input = "1..0y1.0w1.0d1.0h1.0m1.0s" + val actual = TimeUtil.duration(input) + assertNull("Should return null", actual) + } + + + @Test + fun testBadDInputString() { + val libtailscaleWrapperMock = mock(LibtailscaleWrapper::class.java) + doNothing().`when`(libtailscaleWrapperMock).sendLog(anyString(), anyString()) + + + val input = "1.0yy1.0w1.0d1.0h1.0m1.0s" + val actual = TimeUtil.duration(input) + assertNull("Should return null", actual) + } + + + @Test + fun testIgnoreFractionalSeconds() { + val input = "10.9s" + val expectedSeconds = 10 + val expected = Duration.ofSeconds(expectedSeconds.toLong()) + val actual = TimeUtil.duration(input) + assertEquals("Should return $expectedSeconds seconds", expected, actual) + } } + + + diff --git a/libtailscale/callbacks.go b/libtailscale/callbacks.go index e79a173..2ee022a 100644 --- a/libtailscale/callbacks.go +++ b/libtailscale/callbacks.go @@ -20,6 +20,9 @@ var ( // onDNSConfigChanged is notified when the network changes and the DNS config needs to be updated. It receives the updated interface name. onDNSConfigChanged = make(chan string, 1) + + // onLog receives Android logs to be sent to the logger + onLog = make(chan string, 10) ) // ifname is the interface name retrieved from LinkProperties on network change. An empty string is used if there is no network available. diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 7c05166..0275d6f 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -4,6 +4,8 @@ package libtailscale import ( + "log" + _ "golang.org/x/mobile/bind" ) @@ -168,3 +170,13 @@ func RequestVPN(service IPNService) { func ServiceDisconnect(service IPNService) { onDisconnect <- service } + +func SendLog(logstr []byte) { + select { + case onLog <- string(logstr): + // Successfully sent log + default: + // Channel is full, log not sent + log.Printf("Log %v not sent", logstr) // missing argument in original code + } +} diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 7370726..6ae9131 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -134,4 +134,13 @@ func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Lo if filchErr != nil { log.Printf("SetupLogs: filch setup failed: %v", filchErr) } + + go func() { + for { + select { + case logstr := <-onLog: + b.logger.Logf(logstr) + } + } + }() } From f8f2ee029adc06baa3d042bf6f943cc232059d20 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Wed, 25 Sep 2024 11:32:11 -0400 Subject: [PATCH 39/91] android: fix all linter warnings and treat warnings as errors (#521) #Updates tailscale/corp#22284 Fixes and/or explicitly suppresses all linter warnings and we will now fail the build if new warnings are introduced. Signed-off-by: Jonathan Nobels --- android/build.gradle | 4 ++++ .../com/tailscale/ipn/AppSourceChecker.kt | 23 ++++++++++--------- .../com/tailscale/ipn/QuickToggleService.java | 2 ++ .../com/tailscale/ipn/ui/model/Permissions.kt | 2 +- .../java/com/tailscale/ipn/ui/theme/Theme.kt | 4 ++-- .../tailscale/ipn/ui/util/AndroidTVUtil.kt | 2 +- .../tailscale/ipn/ui/view/ExitNodePicker.kt | 2 +- .../ipn/ui/view/MDMSettingsDebugView.kt | 5 +++- .../com/tailscale/ipn/ui/view/MainView.kt | 2 +- .../tailscale/ipn/ui/view/ManagedByView.kt | 6 +++-- .../ipn/ui/viewModel/MainViewModel.kt | 1 + 11 files changed, 33 insertions(+), 20 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 4d76c40..12db7f6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,6 +54,10 @@ android { targetCompatibility JavaVersion.VERSION_17 } + lintOptions { + warningsAsErrors true + } + kotlinOptions { jvmTarget = "17" } diff --git a/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt b/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt index 72e39fc..fd4d4d7 100644 --- a/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt +++ b/android/src/main/java/com/tailscale/ipn/AppSourceChecker.kt @@ -15,20 +15,21 @@ object AppSourceChecker { val packageName = context.packageName Log.d(TAG, "Package name: $packageName") - val installerPackageName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - packageManager.getInstallSourceInfo(packageName).installingPackageName - } else { - packageManager.getInstallerPackageName(packageName) - } + val installerPackageName = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + packageManager.getInstallSourceInfo(packageName).installingPackageName + } else { + @Suppress("deprecation") packageManager.getInstallerPackageName(packageName) + } Log.d(TAG, "Installer package name: $installerPackageName") return when (installerPackageName) { - "com.android.vending" -> "googleplay" - "org.fdroid.fdroid" -> "fdroid" - "com.amazon.venezia" -> "amazon" - null -> "unknown" - else -> "unknown($installerPackageName)" + "com.android.vending" -> "googleplay" + "org.fdroid.fdroid" -> "fdroid" + "com.amazon.venezia" -> "amazon" + null -> "unknown" + else -> "unknown($installerPackageName)" } -} + } } diff --git a/android/src/main/java/com/tailscale/ipn/QuickToggleService.java b/android/src/main/java/com/tailscale/ipn/QuickToggleService.java index 7b8355f..0ac3bd0 100644 --- a/android/src/main/java/com/tailscale/ipn/QuickToggleService.java +++ b/android/src/main/java/com/tailscale/ipn/QuickToggleService.java @@ -60,6 +60,7 @@ public class QuickToggleService extends TileService { } } + @SuppressWarnings("deprecation") @Override public void onClick() { boolean r; @@ -77,6 +78,7 @@ public class QuickToggleService extends TileService { // Request code for opening activity. startActivityAndCollapse(PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); } else { + // Deprecated, but still required for older versions. startActivityAndCollapse(i); } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/Permissions.kt b/android/src/main/java/com/tailscale/ipn/ui/model/Permissions.kt index 8579f0b..10e4367 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/Permissions.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/Permissions.kt @@ -22,7 +22,7 @@ object Permissions { @Composable get() { val permissionStates = rememberMultiplePermissionsState(permissions = all.map { it.name }) - return all.zip(permissionStates.permissions).filter { (permission, state) -> + return all.zip(permissionStates.permissions).filter { (_, state) -> !state.status.isGranted && !state.status.shouldShowRationale } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt index 7426372..6af87cf 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt @@ -44,7 +44,8 @@ fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable // margins in list items. bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)) - val systemUiController = rememberSystemUiController() + // TODO: Migrate to Activity.enableEdgeToEdge + @Suppress("deprecation") val systemUiController = rememberSystemUiController() DisposableEffect(systemUiController, useDarkTheme) { systemUiController.setStatusBarColor(color = colors.surfaceContainer) @@ -446,7 +447,6 @@ val ColorScheme.disabled: Color val ColorScheme.searchBarColors: TextFieldColors @Composable get() { - val defaults = OutlinedTextFieldDefaults.colors() return OutlinedTextFieldDefaults.colors( focusedLeadingIconColor = MaterialTheme.colorScheme.onSurface, unfocusedLeadingIconColor = MaterialTheme.colorScheme.onSurface, diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/AndroidTVUtil.kt b/android/src/main/java/com/tailscale/ipn/ui/util/AndroidTVUtil.kt index bad37f3..b4265d2 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/AndroidTVUtil.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/AndroidTVUtil.kt @@ -15,7 +15,7 @@ import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV object AndroidTVUtil { fun isAndroidTV(): Boolean { val pm = UninitializedApp.get().packageManager - return (pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) || + return (pm.hasSystemFeature(@Suppress("deprecation") PackageManager.FEATURE_TELEVISION) || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt index 19bec62..6a9396f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt @@ -61,7 +61,7 @@ fun ExitNodePicker( if (forcedExitNodeId != null) { Text( text = - managedByOrganization?.let { + managedByOrganization.value?.let { stringResource(R.string.exit_node_mdm_orgname, it) } ?: stringResource(R.string.exit_node_mdm), style = MaterialTheme.typography.bodyMedium, diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt index ec6a68a..ce8e6f4 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -25,7 +25,10 @@ import com.tailscale.ipn.ui.viewModel.IpnViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MDMSettingsDebugView(backToSettings: BackNavigation, model: IpnViewModel = viewModel()) { +fun MDMSettingsDebugView( + backToSettings: BackNavigation, + @Suppress("UNUSED_PARAMETER") model: IpnViewModel = viewModel() +) { Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = backToSettings) }) { innerPadding -> LazyColumn(modifier = Modifier.padding(innerPadding)) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index df19d78..9f7e5e0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -251,7 +251,7 @@ fun MainView( } } - currentPingDevice?.let { peer -> + currentPingDevice?.let { _ -> ModalBottomSheet(onDismissRequest = { viewModel.onPingDismissal() }) { PingView(model = viewModel.pingViewModel) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt index e66e89a..7494e3e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt @@ -23,14 +23,16 @@ import com.tailscale.ipn.R import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.viewModel.IpnViewModel +@Suppress("UNUSED_PARAMETER") @Composable fun ManagedByView(backToSettings: BackNavigation, model: IpnViewModel = viewModel()) { - Scaffold(topBar = { Header(R.string.managed_by, onBack = backToSettings) }) { innerPadding -> + Scaffold(topBar = { Header(R.string.managed_by, onBack = backToSettings) }) { _ -> Column( verticalArrangement = Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically), horizontalAlignment = Alignment.Start, - modifier = Modifier.fillMaxWidth().safeContentPadding().verticalScroll(rememberScrollState())) { + modifier = + Modifier.fillMaxWidth().safeContentPadding().verticalScroll(rememberScrollState())) { val managedByOrganization = MDMSettings.managedByOrganizationName.flow.collectAsState().value.value val managedByCaption = MDMSettings.managedByCaption.flow.collectAsState().value.value diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index cdfaace..f1c7641 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.launch import java.time.Duration class MainViewModelFactory(private val vpnViewModel: VpnViewModel) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(MainViewModel::class.java)) { return MainViewModel(vpnViewModel) as T From 25e7681c3250f5e97d8281010563d22fb57b340a Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:41:48 -0700 Subject: [PATCH 40/91] android: set VPN status in service APIs (#522) This is mainly a no-op; right now we are setting the VPN status when we successfully edit prefs with wantRunning=false, but the VPN status is separate from tailscaled status and reflects the status of the VPN interface. This change moves that status update into the Android Service APIs. Updates tailscale/tailscale#12850 Updates tailscale/tailscale#12489 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/IPNService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index f7cc396..d33bad6 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -71,7 +71,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { } override fun close() { - app.setWantRunning(false) { updateVpnStatus(false) } + app.setWantRunning(false) {} Notifier.setState(Ipn.State.Stopping) stopForeground(STOP_FOREGROUND_REMOVE) Libtailscale.serviceDisconnect(this) @@ -79,11 +79,13 @@ open class IPNService : VpnService(), libtailscale.IPNService { override fun onDestroy() { close() + updateVpnStatus(false) super.onDestroy() } override fun onRevoke() { close() + updateVpnStatus(false) super.onRevoke() } From c10aca720b8fa814c6b710830f92368e20889359 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:47:18 -0700 Subject: [PATCH 41/91] android: don't set vpnService to nil when state is Stopped (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are currently setting vpnService.service to nil: -any time there’s an error with updateTUN -when we exit out of runBackend -if the config is the default config (aka when the ipn state is Stopped) When it gets set to nil, we don’t handle state or config updates by calling updateTUN until after startVPN is called again. The second case never happens because there’s no condition to break out of the loop in runBackend and ctx is uncancelable per the doc for context.Background() In the third case, we should not establish the VPN; the state is already in the correct Stopped state, but there’s no need to set the service to nil and prevent updateTUN from being called. The quick settings tile bug is caused by this third case, where because the saved prefs starts the app up in the Stopped state, the config is set to the default config, and the service is set to nil, and we can't updateTUN until there’s another startVPN call. This PR: -cleans up the updateTUN error handling to be more consistent -removes the IPNService parameter from updateTUN so that vpnService.service is not set to nil in the third case -updates IPNService to use stopSelf and not stopForeground when we disconnect the VPN; the latter only disconnects if there is a memory need Fixes tailscale/tailscale#12489 Signed-off-by: kari-ts --- .../main/java/com/tailscale/ipn/IPNService.kt | 6 +- libtailscale/backend.go | 62 +++++++++++-------- libtailscale/interfaces.go | 2 + libtailscale/net.go | 10 +-- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index d33bad6..db0ca06 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -73,10 +73,14 @@ open class IPNService : VpnService(), libtailscale.IPNService { override fun close() { app.setWantRunning(false) {} Notifier.setState(Ipn.State.Stopping) - stopForeground(STOP_FOREGROUND_REMOVE) + disconnectVPN() Libtailscale.serviceDisconnect(this) } + override fun disconnectVPN(){ + stopSelf() + } + override fun onDestroy() { close() updateVpnStatus(false) diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 247a802..dc5df4c 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "reflect" "runtime/debug" "sync" "sync/atomic" @@ -165,36 +166,24 @@ func (a *App) runBackend(ctx context.Context) error { select { case s := <-stateCh: state = s - if cfg.rcfg != nil && state >= ipn.Starting && vpnService.service != nil { + if state >= ipn.Starting && vpnService.service != nil && b.isConfigNonNilAndDifferent(cfg.rcfg, cfg.dcfg) { // On state change, check if there are router or config changes requiring an update to VPNBuilder - if err := b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg); err != nil { + if err := b.updateTUN(cfg.rcfg, cfg.dcfg); err != nil { if errors.Is(err, errMultipleUsers) { // TODO: surface error to user } - log.Printf("VPN update failed: %v", err) - - mp := new(ipn.MaskedPrefs) - mp.WantRunning = false - mp.WantRunningSet = true - - _, err := a.EditPrefs(*mp) - if err != nil { - log.Printf("localapi edit prefs error %v", err) - } - - b.lastCfg = nil - b.CloseTUNs() + a.closeVpnService(err, b) } } case n := <-netmapCh: networkMap = n case c := <-configs: cfg = c - if b == nil || vpnService.service == nil || cfg.rcfg == nil { + if vpnService.service == nil || !b.isConfigNonNilAndDifferent(cfg.rcfg, cfg.dcfg) { configErrs <- nil break } - configErrs <- b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg) + configErrs <- b.updateTUN(cfg.rcfg, cfg.dcfg) case s := <-onVPNRequested: if vpnService.service != nil && vpnService.service.ID() == s.ID() { // Still the same VPN instance, do nothing @@ -230,12 +219,9 @@ func (a *App) runBackend(ctx context.Context) error { if networkMap != nil { // TODO } - if cfg.rcfg != nil && state >= ipn.Starting { - if err := b.updateTUN(vpnService.service, cfg.rcfg, cfg.dcfg); err != nil { - log.Printf("VPN update failed: %v", err) - vpnService.service.Close() - b.lastCfg = nil - b.CloseTUNs() + if state >= ipn.Starting && b.isConfigNonNilAndDifferent(cfg.rcfg, cfg.dcfg) { + if err := b.updateTUN(cfg.rcfg, cfg.dcfg); err != nil { + a.closeVpnService(err, b) } } case s := <-onDisconnect: @@ -245,9 +231,7 @@ func (a *App) runBackend(ctx context.Context) error { vpnService.service = nil } case i := <-onDNSConfigChanged: - if b != nil { - go b.NetworkChanged(i) - } + go b.NetworkChanged(i) } } } @@ -346,3 +330,29 @@ func (a *App) newBackend(dataDir, directFileRoot string, appCtx AppContext, stor }() return b, nil } + +func (b *backend) isConfigNonNilAndDifferent(rcfg *router.Config, dcfg *dns.OSConfig) bool { + if reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) { + b.logger.Logf("isConfigNonNilAndDifferent: no change to Routes or DNS, ignore") + return false + } + return rcfg != nil +} + +func (a *App) closeVpnService(err error, b *backend) { + log.Printf("VPN update failed: %v", err) + + mp := new(ipn.MaskedPrefs) + mp.WantRunning = false + mp.WantRunningSet = true + + if _, localApiErr := a.EditPrefs(*mp); localApiErr != nil { + log.Printf("localapi edit prefs error %v", localApiErr) + } + + b.lastCfg = nil + b.CloseTUNs() + + vpnService.service.DisconnectVPN() + vpnService.service = nil +} diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 0275d6f..6460c9f 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -80,6 +80,8 @@ type IPNService interface { Close() + DisconnectVPN() + UpdateVpnStatus(bool) } diff --git a/libtailscale/net.go b/libtailscale/net.go index c4cb0a8..85d2ef6 100644 --- a/libtailscale/net.go +++ b/libtailscale/net.go @@ -9,7 +9,6 @@ import ( "log" "net" "net/netip" - "reflect" "runtime/debug" "strings" "syscall" @@ -125,12 +124,7 @@ var googleDNSServers = []netip.Addr{ netip.MustParseAddr("2001:4860:4860::8844"), } -func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.OSConfig) error { - if reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) { - b.logger.Logf("updateTUN: no change to Routes or DNS, ignore") - return nil - } - +func (b *backend) updateTUN(rcfg *router.Config, dcfg *dns.OSConfig) error { b.logger.Logf("updateTUN: changed") defer b.logger.Logf("updateTUN: finished") @@ -147,7 +141,6 @@ func (b *backend) updateTUN(service IPNService, rcfg *router.Config, dcfg *dns.O if len(rcfg.LocalAddrs) == 0 { return nil } - vpnService.service = service builder := vpnService.service.NewBuilder() b.logger.Logf("updateTUN: got new builder") @@ -258,7 +251,6 @@ func closeFileDescriptor() error { func (b *backend) CloseTUNs() { b.lastCfg = nil b.devices.Shutdown() - vpnService.service = nil } // ifname is the interface name retrieved from LinkProperties on network change. If a network is lost, an empty string is passed in. From 957254164864af6da5a375266d41a577810f87d2 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:00:03 -0700 Subject: [PATCH 42/91] android: bumping OSS (#524) OSS and Version updated to 1.75.51-ta70287d32-gc10aca720b8 Signed-off-by: kari-ts --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 8 ++++---- go.toolchain.rev | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 12db7f6..af16146 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.11-t8b962f23d-gf07d419a125" + versionName "1.75.51-ta70287d32-gc10aca720b8" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 5f93650..b024745 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194 + tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c ) require ( diff --git a/go.sum b/go.sum index 384903a..12be8c5 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194 h1:WkfloEUujoWp7t5kxxktp2BhCaBewWAYHyNyz6WqkfQ= -tailscale.com v1.75.0-pre.0.20240917164848-8b962f23d194/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg= +tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c h1:aEZtf+JelE6YALUHX3r9Hw/iRoaD2l1WVBMHtnS9yXY= +tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= diff --git a/go.toolchain.rev b/go.toolchain.rev index 22ed8ff..5d87594 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -ed9dc37b2b000f376a3e819cbb159e2c17a2dac6 \ No newline at end of file +bf15628b759344c6fc7763795a405ba65b8be5d7 From 2f08e2f02d793c630a3887f0627b914e1487e9de Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 1 Oct 2024 19:29:15 +0200 Subject: [PATCH 43/91] libtailscale: add metrics to NewUserspaceEngine (#525) Updates tailscale/corp#22075 Signed-off-by: Kristoffer Dalby --- libtailscale/backend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/libtailscale/backend.go b/libtailscale/backend.go index dc5df4c..f9e923f 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -286,6 +286,7 @@ func (a *App) newBackend(dataDir, directFileRoot string, appCtx AppContext, stor SetSubsystem: sys.Set, NetMon: b.netMon, HealthTracker: sys.HealthTracker(), + Metrics: sys.UserMetricsRegistry(), DriveForLocal: driveimpl.NewFileSystemForLocal(logf), }) if err != nil { From 8eabe8d6dd4b2a32cee487b07d155d9f374c212f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 1 Oct 2024 19:51:43 +0200 Subject: [PATCH 44/91] android: bumping OSS (#526) android: bumping OSS OSS and Version updated to 1.75.56-t1eaad7d3d-g625f6f02352 Signed-off-by: Kristoffer Dalby --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index af16146..03e27b6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.51-ta70287d32-gc10aca720b8" + versionName "1.75.56-t1eaad7d3d-g625f6f02352" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index b024745..f22daa0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c + tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0 ) require ( diff --git a/go.sum b/go.sum index 12be8c5..97c5868 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c h1:aEZtf+JelE6YALUHX3r9Hw/iRoaD2l1WVBMHtnS9yXY= -tailscale.com v1.75.0-pre.0.20240930093608-a70287d3248c/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= +tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0 h1:oU86zCxNfBwdJ77FFWULisLK/+E6BAuYaWJ9lICf/6o= +tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= From f5ecca3c9672b054f464676af3e4535137758ced Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:14:22 -0700 Subject: [PATCH 45/91] android: StringArrayListMDMSetting should check for String[] (#527) getFromBundle should check for both String[] and ArrayList Fixes tailscale/corp#23557 Signed-off-by: kari-ts --- .../ipn/mdm/MDMSettingsDefinitions.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt index 1ddf59c..173159f 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt @@ -46,9 +46,26 @@ class StringMDMSetting(key: String, localizedTitle: String) : class StringArrayListMDMSetting(key: String, localizedTitle: String) : MDMSetting?>(null, key, localizedTitle) { - override fun getFromBundle(bundle: Bundle) = bundle.getStringArrayList(key) - override fun getFromPrefs(prefs: SharedPreferences) = - prefs.getStringSet(key, HashSet())?.toList() + override fun getFromBundle(bundle: Bundle): List? { + // Try to retrieve the value as a String[] first + val stringArray = bundle.getStringArray(key) + if (stringArray != null) { + return stringArray.toList() + } + + // Optionally, handle other types if necessary + val stringArrayList = bundle.getStringArrayList(key) + if (stringArrayList != null) { + return stringArrayList + } + + // If neither String[] nor ArrayList is found, return null + return null + } + + override fun getFromPrefs(prefs: SharedPreferences): List? { + return prefs.getStringSet(key, HashSet())?.toList() + } } class AlwaysNeverUserDecidesMDMSetting(key: String, localizedTitle: String) : From be89cb10fe3c490a751e2b05969058ea1ac37e08 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:56:39 -0700 Subject: [PATCH 46/91] android: bumping OSS (#528) OSS and Version updated to 1.75.58-t262c526c4-gf5ecca3c967 Signed-off-by: kari-ts --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 03e27b6..7c3f0ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.56-t1eaad7d3d-g625f6f02352" + versionName "1.75.58-t262c526c4-gf5ecca3c967" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index f22daa0..ab2da88 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0 + tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc ) require ( diff --git a/go.sum b/go.sum index 97c5868..f2c2a0d 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0 h1:oU86zCxNfBwdJ77FFWULisLK/+E6BAuYaWJ9lICf/6o= -tailscale.com v1.75.0-pre.0.20241001165820-1eaad7d3deb0/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= +tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc h1:+Pq+F1fMBfVUC8hupODw4GX+9OS2Ag9Zf66+2Af2WWE= +tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= From 2daeee584dfff57e876c46d7c27aaf4742f8d1ad Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 4 Oct 2024 11:48:36 -0700 Subject: [PATCH 47/91] ui/UserView: show custom control server URL in account switcher (#529) --- .../com/tailscale/ipn/ui/model/IpnState.kt | 21 +++++++++++++++++ .../com/tailscale/ipn/ui/view/UserView.kt | 23 ++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt index f54e8f4..e66fea7 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt @@ -4,6 +4,7 @@ package com.tailscale.ipn.ui.model import kotlinx.serialization.Serializable +import java.net.URL class IpnState { @Serializable @@ -123,9 +124,29 @@ class IpnLocal { val UserProfile: Tailcfg.UserProfile, val NetworkProfile: Tailcfg.NetworkProfile? = null, val LocalUserID: String, + var ControlURL: String? = null, ) { fun isEmpty(): Boolean { return ID.isEmpty() } + + // Returns true if the profile uses a custom control server (not Tailscale SaaS). + fun isUsingCustomControlServer(): Boolean { + return ControlURL != null && ControlURL != "controlplane.tailscale.com" + } + + // Returns the hostname of the custom control server, if any was set. + // + // Returns null if the ControlURL provided by the backend is an invalid URL, and + // a hostname cannot be extracted. + fun customControlServerHostname(): String? { + if (!isUsingCustomControlServer()) return null + + return try { + URL(ControlURL).host + } catch (e: Exception) { + null + } + } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt index 2279a8e..0c2a3dc 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt @@ -5,6 +5,7 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.offset import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -54,17 +55,27 @@ fun UserView( leadingContent = { Avatar(profile = profile, size = 36) }, headlineContent = { AutoResizingText( - text = profile.UserProfile.DisplayName, + text = profile.UserProfile.LoginName, style = MaterialTheme.typography.titleMedium.short, minFontSize = MaterialTheme.typography.minTextSize, overflow = TextOverflow.Ellipsis) }, supportingContent = { - AutoResizingText( - text = profile.NetworkProfile?.DomainName ?: "", - style = MaterialTheme.typography.bodyMedium.short, - minFontSize = MaterialTheme.typography.minTextSize, - overflow = TextOverflow.Ellipsis) + Column { + AutoResizingText( + text = profile.NetworkProfile?.DomainName ?: "", + style = MaterialTheme.typography.bodyMedium.short, + minFontSize = MaterialTheme.typography.minTextSize, + overflow = TextOverflow.Ellipsis) + + profile.customControlServerHostname()?.let { + AutoResizingText( + text = it, + style = MaterialTheme.typography.bodyMedium.short, + minFontSize = MaterialTheme.typography.minTextSize, + overflow = TextOverflow.Ellipsis) + } + } }, trailingContent = { when (actionState) { From 4ca757bb7567f2754c545f3480166e07119265a0 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 4 Oct 2024 11:57:50 -0700 Subject: [PATCH 48/91] android: bumping OSS to 1.75.80 (#530) android: bumping OSS OSS and Version updated to 1.75.80-t8fdffb8da-g2daeee584df Signed-off-by: Andrea Gottardo --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7c3f0ca..04b0d0d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.58-t262c526c4-gf5ecca3c967" + versionName "1.75.80-t8fdffb8da-g2daeee584df" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index ab2da88..f71ee49 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc + tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d ) require ( diff --git a/go.sum b/go.sum index f2c2a0d..f34a100 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc h1:+Pq+F1fMBfVUC8hupODw4GX+9OS2Ag9Zf66+2Af2WWE= -tailscale.com v1.75.0-pre.0.20241001211147-262c526c4efc/go.mod h1:7thJue8VqOg3kbRY2fmnqspw4ZfgTXskNtDnvEgAmfA= +tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d h1:c8rM8R1JUXdgC/GWbnXNs4a+LkjS1K1biMi3AK9KQac= +tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From 0126db799b191733ef4718905e967328db5de06a Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 4 Oct 2024 12:47:24 -0700 Subject: [PATCH 49/91] ui/model: adjust default control server URL (#531) Updates tailscale/corp#23660 I screwed up by not including 'https://' in a last-minute refactoring :-) --- android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt index e66fea7..af36f21 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt @@ -131,8 +131,8 @@ class IpnLocal { } // Returns true if the profile uses a custom control server (not Tailscale SaaS). - fun isUsingCustomControlServer(): Boolean { - return ControlURL != null && ControlURL != "controlplane.tailscale.com" + private fun isUsingCustomControlServer(): Boolean { + return ControlURL != null && ControlURL != "https://controlplane.tailscale.com" } // Returns the hostname of the custom control server, if any was set. From a32c2aa0df907563b340f41da913ac12a53783d7 Mon Sep 17 00:00:00 2001 From: Keli <104461838+kelivel@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:20:10 -0400 Subject: [PATCH 50/91] android: bumping OSS (#532) OSS and Version updated to 1.75.81-t4ad3f0122-g0126db799b1 Signed-off-by: Keli Velazquez --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 04b0d0d..4d125ba 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.80-t8fdffb8da-g2daeee584df" + versionName "1.75.81-t4ad3f0122-g0126db799b1" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index f71ee49..e7b4478 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d + tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574 ) require ( diff --git a/go.sum b/go.sum index f34a100..3095a6f 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d h1:c8rM8R1JUXdgC/GWbnXNs4a+LkjS1K1biMi3AK9KQac= -tailscale.com v1.75.0-pre.0.20241004163519-8fdffb8da05d/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574 h1:Ql1Ojbp3j2u1bekVmDqo0fhBkM0jXA1trjtJi/zbuaw= +tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From cd993fee4342ddddeb00957a98a293945dca0b90 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Fri, 4 Oct 2024 14:12:46 -0700 Subject: [PATCH 51/91] ui: hide commit hashes in user-facing version string (#534) We currently show the full version number everywhere. This pointlessly causes confusion for users, and is only really useful for Tailscale employees. Let's show the marketing version everywhere instead. Users can still tap on the version number to copy the full version string. The extended version is also available in the Android settings, when inspecting Tailscale from the Apps list. Signed-off-by: Andrea Gottardo --- .../com/tailscale/ipn/ui/util/AppVersion.kt | 20 +++++++++++++++++++ .../com/tailscale/ipn/ui/view/AboutView.kt | 8 +++++++- .../com/tailscale/ipn/ui/view/SettingsView.kt | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/util/AppVersion.kt diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/AppVersion.kt b/android/src/main/java/com/tailscale/ipn/ui/util/AppVersion.kt new file mode 100644 index 0000000..fb042fb --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/util/AppVersion.kt @@ -0,0 +1,20 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.util + +import com.tailscale.ipn.BuildConfig + +class AppVersion { + companion object { + // Returns the short version of the build version, which is what users typically expect. + // For instance, if the build version is "1.75.80-t8fdffb8da-g2daeee584df", + // this function returns "1.75.80". + fun Short(): String { + // Split the full version string by hyphen (-) + val parts = BuildConfig.VERSION_NAME.split("-") + // Return only the part before the first hyphen + return parts[0] + } + } +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/AboutView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/AboutView.kt index 7640b6c..fb0c098 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/AboutView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/AboutView.kt @@ -33,6 +33,7 @@ import com.tailscale.ipn.BuildConfig import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.theme.logoBackground +import com.tailscale.ipn.ui.util.AppVersion @Composable fun AboutView(backToSettings: BackNavigation) { @@ -69,9 +70,14 @@ fun AboutView(backToSettings: BackNavigation) { Text( modifier = Modifier.clickable { + // When users tap on the version number, the extended version string + // (including commit hashes) is copied to the clipboard. + // This may be useful for debugging purposes... localClipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME)) }, - text = "${stringResource(R.string.version)} ${BuildConfig.VERSION_NAME}", + // ... but we always display the short version in the UI to avoid user + // confusion. + text = "${stringResource(R.string.version)} ${AppVersion.Short()}", fontWeight = MaterialTheme.typography.bodyMedium.fontWeight, fontSize = MaterialTheme.typography.bodyMedium.fontSize) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index d83f50d..c425417 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -39,6 +39,7 @@ import com.tailscale.ipn.ui.viewModel.SettingsNav import com.tailscale.ipn.ui.viewModel.SettingsViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.notifier.Notifier +import com.tailscale.ipn.ui.util.AppVersion @Composable fun SettingsView(settingsNav: SettingsNav, viewModel: SettingsViewModel = viewModel(), vpnViewModel: VpnViewModel = viewModel()) { @@ -112,7 +113,7 @@ fun SettingsView(settingsNav: SettingsNav, viewModel: SettingsViewModel = viewMo Lists.ItemDivider() Setting.Text( R.string.about_tailscale, - subtitle = "${stringResource(id = R.string.version)} ${BuildConfig.VERSION_NAME}", + subtitle = "${stringResource(id = R.string.version)} ${AppVersion.Short()}", onClick = settingsNav.onNavigateToAbout) // TODO: put a heading for the debug section From d309f31b5ab868b79fa1e63e1a00bf4bb090d809 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:02:55 -0700 Subject: [PATCH 52/91] android: don't show hex code yet (#536) Hold off on showing the code until there is a place in the admin console for the user to input the code. Updates tailscale/tailscale#13277 Signed-off-by: kari-ts --- .../ipn/ui/viewModel/LoginQRViewModel.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt index 34d88df..7499fc0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt @@ -22,21 +22,26 @@ class LoginQRViewModel : IpnViewModel() { val numCode: StateFlow = MutableStateFlow(null) val qrCode: StateFlow = MutableStateFlow(null) + // Remove this once changes to admin console allowing input code to be entered are made. + val codeEnabled = false init { viewModelScope.launch { Notifier.browseToURL.collect { url -> url?.let { qrCode.set(generateQRCode(url, 200, 0)) - // Extract the string after "https://login.tailscale.com/a/" - val prefix = "https://login.tailscale.com/a/" - val code = - if (it.startsWith(prefix)) { - it.removePrefix(prefix) - } else { - null - } - numCode.set(code) + + if (codeEnabled) { + // Extract the string after "https://login.tailscale.com/a/" + val prefix = "https://login.tailscale.com/a/" + val code = + if (it.startsWith(prefix)) { + it.removePrefix(prefix) + } else { + null + } + numCode.set(code) + } } ?: run { qrCode.set(null) From 5f19730c7a4be7d1bfde5d2077e2820027aa067a Mon Sep 17 00:00:00 2001 From: Keli <104461838+kelivel@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:00:49 -0400 Subject: [PATCH 53/91] android: bumping OSS (#537) OSS and Version updated to 1.75.104-tf6d4d0335-gd309f31b5ab Signed-off-by: Keli Velazquez --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 4d125ba..2d52502 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.81-t4ad3f0122-g0126db799b1" + versionName "1.75.104-tf6d4d0335-gd309f31b5ab" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index e7b4478..8392f6a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574 + tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb ) require ( diff --git a/go.sum b/go.sum index 3095a6f..034e3aa 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574 h1:Ql1Ojbp3j2u1bekVmDqo0fhBkM0jXA1trjtJi/zbuaw= -tailscale.com v1.75.0-pre.0.20241004185700-4ad3f0122574/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb h1:Sj+gV1UTEu0C72EI3rSGoSJPE/i+qTKgnUWgtb611y8= +tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From 47cde89984e90f3082ca5251fa00e8243c96520a Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Thu, 10 Oct 2024 12:44:42 -0700 Subject: [PATCH 54/91] android: update dependencies (#535) Updates #cleanup Bumps our project dependencies to the latest versions. Verified that the project builds properly. Signed-off-by: Andrea Gottardo --- android/build.gradle | 24 +++++++++---------- .../java/com/tailscale/ipn/ui/view/Avatar.kt | 4 ++-- .../com/tailscale/ipn/ui/view/SharedViews.kt | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 2d52502..f9c359c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -11,7 +11,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.5.1' + classpath 'com.android.tools.build:gradle:8.6.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath("com.ncorti.ktfmt.gradle:plugin:0.17.0") @@ -112,7 +112,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.13.1' implementation "androidx.browser:browser:1.8.0" implementation "androidx.security:security-crypto:1.1.0-alpha06" - implementation "androidx.work:work-runtime:2.9.0" + implementation "androidx.work:work-runtime:2.9.1" // Kotlin dependencies. implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" @@ -123,20 +123,20 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" // Compose dependencies. - def composeBom = platform('androidx.compose:compose-bom:2024.06.00') + def composeBom = platform('androidx.compose:compose-bom:2024.09.03') implementation composeBom - implementation 'androidx.compose.material3:material3:1.2.1' - implementation 'androidx.compose.material:material-icons-core:1.6.8' - implementation "androidx.compose.ui:ui:1.6.8" - implementation "androidx.compose.ui:ui-tooling:1.6.8" - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3' - implementation 'androidx.activity:activity-compose:1.9.0' + implementation 'androidx.compose.material3:material3:1.3.0' + implementation 'androidx.compose.material:material-icons-core:1.7.3' + implementation "androidx.compose.ui:ui:1.7.3" + implementation "androidx.compose.ui:ui-tooling:1.7.3" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' + implementation 'androidx.activity:activity-compose:1.9.2' implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" implementation "androidx.core:core-splashscreen:1.1.0-rc01" // Navigation dependencies. - def nav_version = "2.7.7" + def nav_version = "2.8.2" implementation "androidx.navigation:navigation-compose:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" @@ -151,7 +151,7 @@ dependencies { // Integration Tests androidTestImplementation composeBom - androidTestImplementation 'androidx.test:runner:1.6.1' + androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' @@ -164,7 +164,7 @@ dependencies { // Unit Tests testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.4.0' + testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.mockito:mockito-inline:5.2.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index 9f64878..2e5662a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -33,7 +33,7 @@ fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit) modifier = modifier.clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), onClick = action) } Icon( diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt index de31170..8d0ef3a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -21,6 +20,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -76,7 +76,7 @@ fun BackArrow(action: () -> Unit, focusRequester: FocusRequester) { Modifier.focusRequester(focusRequester) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), onClick = { action() })) } } From 8ff0672ec7897381cf515abbf2b63be29d4f6b14 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Thu, 10 Oct 2024 16:06:39 -0400 Subject: [PATCH 55/91] android: bumping OSS (#540) OSS and Version updated to 1.77.0-tacb4a22dc-g5f19730c7a4 Signed-off-by: Jonathan Nobels --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index f9c359c..d51241d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.75.104-tf6d4d0335-gd309f31b5ab" + versionName "1.77.0-tacb4a22dc-g5f19730c7a4" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 8392f6a..f8992a1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb + tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d ) require ( diff --git a/go.sum b/go.sum index 034e3aa..4fecc98 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb h1:Sj+gV1UTEu0C72EI3rSGoSJPE/i+qTKgnUWgtb611y8= -tailscale.com v1.75.0-pre.0.20241009122300-f6d4d03355eb/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d h1:wirHia3zSR0OxyVavFmY6sjxgofs6raRRuUZllip6hg= +tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From 753b8d3fb4b34b65b01a3b0ea0c227c3ba341ad8 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:27:25 -0700 Subject: [PATCH 56/91] android: handle multiple redundant intents (#541) Use FLAG_UPDATE_CURRENT for managing multiple calls to startForegroundService. This ensures only one instance of the intent is active and replaces any previously pending intents with the latest one. Fixes tailscale/corp#23828 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index dd89972..f0b3dde 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -371,16 +371,27 @@ open class UninitializedApp : Application() { fun startVPN() { val intent = Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_START_VPN } + // FLAG_UPDATE_CURRENT ensures that if the intent is already pending, the existing intent will + // be updated rather than creating multiple redundant instances. + val pendingIntent = + PendingIntent.getService( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_IMMUTABLE // FLAG_IMMUTABLE for Android 12+ + ) + try { - startForegroundService(intent) + pendingIntent.send() } catch (foregroundServiceStartException: IllegalStateException) { TSLog.e( TAG, - "startVPN hit ForegroundServiceStartNotAllowedException in startForegroundService(): $foregroundServiceStartException") + "startVPN hit ForegroundServiceStartNotAllowedException: $foregroundServiceStartException") } catch (securityException: SecurityException) { - TSLog.e(TAG, "startVPN hit SecurityException in startForegroundService(): $securityException") + TSLog.e(TAG, "startVPN hit SecurityException: $securityException") } catch (e: Exception) { - TSLog.e(TAG, "startVPN hit exception in startForegroundService(): $e") + TSLog.e(TAG, "startVPN hit exception: $e") } } From 83f3f737ad743ba175e05c2e5f73b3e0ac1a00fb Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:28:58 -0700 Subject: [PATCH 57/91] android: bump OSS (#542) OSS and Version updated to 1.77.12-ta8f9c0d6e-g753b8d3fb4b Signed-off-by: kari-ts --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index d51241d..d2b1ad6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.77.0-tacb4a22dc-g5f19730c7a4" + versionName "1.77.12-ta8f9c0d6e-g753b8d3fb4b" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index f8992a1..9c44a3b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d + tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a ) require ( diff --git a/go.sum b/go.sum index 4fecc98..f674216 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d h1:wirHia3zSR0OxyVavFmY6sjxgofs6raRRuUZllip6hg= -tailscale.com v1.75.0-pre.0.20241010183414-acb4a22dcc1d/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a h1:nGLKfmPOA8dmMqGUjBDIaUD4RkXo+QpP3TXoAKVwvHU= +tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From 18b8c78754baac3e6c48ffbcac73804a6946273f Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:35:45 -0700 Subject: [PATCH 58/91] android: update PR description for bumposs to use "bump" (#543) Updates #cleanup Signed-off-by: kari-ts --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d6d3a41..7257bb8 100644 --- a/Makefile +++ b/Makefile @@ -208,7 +208,7 @@ tag_release: ## Tag the current commit with the current version .PHONY: bumposs ## Bump to the latest oss and update teh versions. bumposs: update-oss update-version - git commit -sm "android: bumping OSS" -m "OSS and Version updated to ${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum + git commit -sm "android: bump OSS" -m "OSS and Version updated to ${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" .PHONY: bump_version_code From 6ec54234ef3bd5f70a74737da5b91f06d35be4ca Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:54:20 -0700 Subject: [PATCH 59/91] android: fix avatar focusable (#538) -Apply focusable() directly to outer Box instead of nested with clickable() -Explicitly handle focus -Simplify focusable area Fixes tailscale/corp/#23762 Signed-off-by: kari-ts Signed-off-by: kari-ts <135075563+kari-ts@users.noreply.github.com> --- .../java/com/tailscale/ipn/ui/view/Avatar.kt | 74 +++++++++++++++---- .../com/tailscale/ipn/ui/view/MainView.kt | 29 ++------ 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index 2e5662a..baae957 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -3,20 +3,28 @@ package com.tailscale.ipn.ui.view +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.annotation.ExperimentalCoilApi @@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal @OptIn(ExperimentalCoilApi::class) @Composable fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) { - var modifier = Modifier.size((size * .8f).dp) - action?.let { - modifier = - modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = action) - } - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(R.string.settings_title), - modifier = modifier) + var isFocused = remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current - profile?.UserProfile?.ProfilePicURL?.let { url -> - AsyncImage(model = url, modifier = Modifier.size((size * 1.2f).dp), contentDescription = null) + // Outer Box for the larger focusable and clickable area + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(4.dp) + .size((size * 1.5f).dp) // Focusable area is larger than the avatar + .clip(CircleShape) // Ensure both the focus and click area are circular + .background( + if (isFocused.value) MaterialTheme.colorScheme.surface + else Color.Transparent, + ) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } + .focusable() // Make this outer Box focusable (after onFocusChanged) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = true), // Apply ripple effect inside circular bounds + onClick = { + action?.invoke() + focusManager.clearFocus() // Clear focus after clicking the avatar + } + ) + ) { + // Inner Box to hold the avatar content (Icon or AsyncImage) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(size.dp) + .clip(CircleShape) + ) { + if (profile?.UserProfile?.ProfilePicURL != null) { + AsyncImage( + model = profile.UserProfile.ProfilePicURL, + modifier = Modifier.size(size.dp).clip(CircleShape), + contentDescription = null + ) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(R.string.settings_title), + modifier = Modifier + .size((size * 0.8f).dp) + .clip(CircleShape) // Icon size slightly smaller than the Box + ) + } + } } - } } + diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 9f7e5e0..6966383 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown @@ -187,28 +186,14 @@ fun MainView( } }, trailingContent = { - Box( - modifier = - Modifier.weight(1f) - .focusable() - .clickable { navigation.onNavigateToSettings() } - .padding(8.dp), - contentAlignment = Alignment.CenterEnd) { - when (user) { - null -> SettingsButton { navigation.onNavigateToSettings() } - else -> - Box( - contentAlignment = Alignment.Center, - modifier = - Modifier.size(42.dp).clip(CircleShape).focusable().clickable { - navigation.onNavigateToSettings() - }) { - Avatar(profile = user, size = 36) { - navigation.onNavigateToSettings() - } - } - } + Box(modifier = Modifier.padding(8.dp), contentAlignment = Alignment.CenterEnd) { + when (user) { + null -> SettingsButton { navigation.onNavigateToSettings() } + else -> { + Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() } } + } + } }) when (state) { From 354a903ee1d093fd1bd7a33897975d8e073edfe5 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:32:24 -0700 Subject: [PATCH 60/91] android: make tailnet lock setup view focusable and clickable (#544) -use a shared InteractionSource for focusing and clicking to ensure they rely on the same state and to coordinate so that visual feedback is shown on scroll without affecting the click InteractionSource -use LocalIndication to ensure that the click interaction maintains the visual feedback when combined with focusable -use onFocusChanged to explicitly track the focus state Updates tailscale/corp#21737 Signed-off-by: kari-ts --- .../ipn/ui/util/ClipboardValueView.kt | 67 ++++++++-------- .../ipn/ui/view/TailnetLockSetupView.kt | 76 ++++++++++--------- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt index 90e7b79..865282f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt @@ -3,52 +3,59 @@ package com.tailscale.ipn.ui.util +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import com.tailscale.ipn.R -import com.tailscale.ipn.ui.theme.titledListItem @Composable fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) { - val localClipboardManager = LocalClipboardManager.current - val modifier = - Modifier.focusable() - .clickable { - localClipboardManager.setText(AnnotatedString(value)) - } + val isFocused = remember { mutableStateOf(false) } + val localClipboardManager = LocalClipboardManager.current + val interactionSource = remember { MutableInteractionSource() } - ListItem( - colors = MaterialTheme.colorScheme.titledListItem, - modifier = modifier, - overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } }, - headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) }, - supportingContent = - subtitle?.let { - { - Text( - it, - modifier = Modifier.padding(top = 8.dp), - style = MaterialTheme.typography.bodyMedium) - } - }, - trailingContent = { - Icon( - painterResource(R.drawable.clipboard), - stringResource(R.string.copy_to_clipboard), - modifier = Modifier.width(24.dp).height(24.dp)) - }) -} + ListItem( + modifier = Modifier + .focusable(interactionSource = interactionSource) + .onFocusChanged { focusState -> isFocused.value = focusState.isFocused } + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current + ) { localClipboardManager.setText(AnnotatedString(value)) } + .background( + if (isFocused.value) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f) + else Color.Transparent + ), + overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } }, + headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) }, + supportingContent = subtitle?.let { + { Text(it, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium) } + }, + trailingContent = { + Icon( + painterResource(R.drawable.clipboard), + contentDescription = stringResource(R.string.copy_to_clipboard), + modifier = Modifier.size(24.dp) + ) + } + ) +} \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index d21e6af..bf3cb79 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -3,13 +3,15 @@ package com.tailscale.ipn.ui.view +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.ClickableText -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -18,7 +20,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -52,40 +56,44 @@ fun TailnetLockSetupView( Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> LoadingIndicator.Wrap { - Column( - modifier = - Modifier.padding(innerPadding) - .focusable() - .verticalScroll(rememberScrollState()) - .fillMaxSize()) { - ExplainerView() + LazyColumn(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + item { ExplainerView() } - statusItems.forEach { statusItem -> - Lists.ItemDivider() + items(statusItems) { statusItem -> + val interactionSource = remember { MutableInteractionSource() } + ListItem( + modifier = + Modifier.focusable( + interactionSource = interactionSource) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current + ) {}, + leadingContent = { + Icon( + painter = painterResource(id = statusItem.icon), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant) + }, + headlineContent = { Text(stringResource(statusItem.title)) }) + } - ListItem( - leadingContent = { - Icon( - painter = painterResource(id = statusItem.icon), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant) - }, - headlineContent = { Text(stringResource(statusItem.title)) }) - } - // Node key - Lists.SectionDivider() - ClipboardValueView( - value = nodeKey, - title = stringResource(R.string.node_key), - subtitle = stringResource(R.string.node_key_explainer)) + item { + // Node key section + Lists.SectionDivider() + ClipboardValueView( + value = nodeKey, + title = stringResource(R.string.node_key), + subtitle = stringResource(R.string.node_key_explainer)) - // Tailnet lock key - Lists.SectionDivider() - ClipboardValueView( - value = tailnetLockTlPubKey, - title = stringResource(R.string.tailnet_lock_key), - subtitle = stringResource(R.string.tailnet_lock_key_explainer)) - } + // Tailnet lock key section + Lists.SectionDivider() + ClipboardValueView( + value = tailnetLockTlPubKey, + title = stringResource(R.string.tailnet_lock_key), + subtitle = stringResource(R.string.tailnet_lock_key_explainer)) + } + } } } } From 2e9f6b735efcdedcb9e5f29f677f7db0c50470d4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 18 Oct 2024 11:11:03 -0700 Subject: [PATCH 61/91] Makefile: fix typo in comment Updates #cleanup Signed-off-by: Brad Fitzpatrick --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7257bb8..554d6d3 100644 --- a/Makefile +++ b/Makefile @@ -206,7 +206,7 @@ tag_release: ## Tag the current commit with the current version git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" -.PHONY: bumposs ## Bump to the latest oss and update teh versions. +.PHONY: bumposs ## Bump to the latest oss and update the versions. bumposs: update-oss update-version git commit -sm "android: bump OSS" -m "OSS and Version updated to ${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" From af98b14770ea8eaee62aec24e5507b6aa28cd8ad Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:59:15 -0700 Subject: [PATCH 62/91] android: hide disconnect action if force enabled (#539) In notification, don't show 'Disconnect' button if MDM force enable is on. Fixes tailscale/corp#23764 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 70 ++++++++++++------- .../main/java/com/tailscale/ipn/IPNService.kt | 29 ++++++-- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index f0b3dde..5126626 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -141,17 +143,32 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { initViewModels() applicationScope.launch { Notifier.state.collect { state -> - val ableToStartVPN = state > Ipn.State.NeedsMachineAuth - // If VPN is stopped, show a disconnected notification. If it is running as a foregrround - // service, IPNService will show a connected notification. - if (state == Ipn.State.Stopped) { - notifyStatus(false) - } - val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running - updateConnStatus(ableToStartVPN) - QuickToggleService.setVPNRunning(vpnRunning) + combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled -> + Pair(state, forceEnabled) + } + .collect { (state, hideDisconnectAction) -> + val ableToStartVPN = state > Ipn.State.NeedsMachineAuth + // If VPN is stopped, show a disconnected notification. If it is running as a + // foreground + // service, IPNService will show a connected notification. + if (state == Ipn.State.Stopped) { + notifyStatus(vpnRunning = false, hideDisconnectAction = hideDisconnectAction.value) + } + + val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running + updateConnStatus(ableToStartVPN) + QuickToggleService.setVPNRunning(vpnRunning) + + // Update notification status when VPN is running + if (vpnRunning) { + notifyStatus(vpnRunning = true, hideDisconnectAction = hideDisconnectAction.value) + } + } } } + applicationScope.launch { + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + } } private fun initViewModels() { @@ -419,8 +436,8 @@ open class UninitializedApp : Application() { notificationManager.createNotificationChannel(channel) } - fun notifyStatus(vpnRunning: Boolean) { - notifyStatus(buildStatusNotification(vpnRunning)) + fun notifyStatus(vpnRunning: Boolean, hideDisconnectAction: Boolean) { + notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction)) } fun notifyStatus(notification: Notification) { @@ -438,7 +455,7 @@ open class UninitializedApp : Application() { notificationManager.notify(STATUS_NOTIFICATION_ID, notification) } - fun buildStatusNotification(vpnRunning: Boolean): Notification { + fun buildStatusNotification(vpnRunning: Boolean, hideDisconnectAction: Boolean): Notification { val message = getString(if (vpnRunning) R.string.connected else R.string.not_connected) val icon = if (vpnRunning) R.drawable.ic_notification else R.drawable.ic_notification_disabled val action = @@ -460,19 +477,22 @@ open class UninitializedApp : Application() { PendingIntent.getActivity( this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - return NotificationCompat.Builder(this, STATUS_CHANNEL_ID) - .setSmallIcon(icon) - .setContentTitle("Tailscale") - .setContentText(message) - .setAutoCancel(!vpnRunning) - .setOnlyAlertOnce(!vpnRunning) - .setOngoing(vpnRunning) - .setSilent(true) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build()) - .setContentIntent(pendingIntent) - .build() + val builder = + NotificationCompat.Builder(this, STATUS_CHANNEL_ID) + .setSmallIcon(icon) + .setContentTitle(getString(R.string.app_name)) + .setContentText(message) + .setAutoCancel(!vpnRunning) + .setOnlyAlertOnce(!vpnRunning) + .setOngoing(vpnRunning) + .setSilent(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + if (!vpnRunning || !hideDisconnectAction) { + builder.addAction( + NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build()) + } + return builder.build() } fun addUserDisallowedPackageName(packageName: String) { diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index db0ca06..920d08d 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -12,6 +12,10 @@ import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.util.TSLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import libtailscale.Libtailscale import java.util.UUID @@ -19,6 +23,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { private val TAG = "IPNService" private val randomID: String = UUID.randomUUID().toString() private lateinit var app: App + val scope = CoroutineScope(Dispatchers.IO) override fun id(): String { return randomID @@ -42,7 +47,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { START_NOT_STICKY } ACTION_START_VPN -> { - showForegroundNotification() + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + showForegroundNotification(hideDisconnectAction.value) + } app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY @@ -51,7 +60,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { // This means we were started by Android due to Always On VPN. // We show a non-foreground notification because we weren't // started as a foreground service. - app.notifyStatus(true) + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + app.notifyStatus(true, hideDisconnectAction.value) + } app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY @@ -60,7 +73,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { // This means that we were restarted after the service was killed // (potentially due to OOM). if (UninitializedApp.get().isAbleToStartVPN()) { - showForegroundNotification() + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + showForegroundNotification(hideDisconnectAction.value) + } App.get() Libtailscale.requestVPN(this) START_STICKY @@ -77,7 +94,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { Libtailscale.serviceDisconnect(this) } - override fun disconnectVPN(){ + override fun disconnectVPN() { stopSelf() } @@ -97,11 +114,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { app.getAppScopedViewModel().setVpnPrepared(isPrepared) } - private fun showForegroundNotification() { + private fun showForegroundNotification(hideDisconnectAction: Boolean) { try { startForeground( UninitializedApp.STATUS_NOTIFICATION_ID, - UninitializedApp.get().buildStatusNotification(true)) + UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction)) } catch (e: Exception) { TSLog.e(TAG, "Failed to start foreground service: $e") } From cafb114ae0ae3aedd6fad1915b2d9d8fce9d7055 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:16:44 -0700 Subject: [PATCH 63/91] android: don't show permissions for TV (#548) Android TV has limited support for notifications compared to mobile - notifications are not show in the system UI to provide a leanback experience. Remove 'Permissions' from Settings menu. Fixes tailscale/corp/#21034 Signed-off-by: kari-ts --- .../main/java/com/tailscale/ipn/ui/view/SettingsView.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index c425417..b89df15 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -39,6 +39,7 @@ import com.tailscale.ipn.ui.viewModel.SettingsNav import com.tailscale.ipn.ui.viewModel.SettingsViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.notifier.Notifier +import com.tailscale.ipn.ui.util.AndroidTVUtil import com.tailscale.ipn.ui.util.AppVersion @Composable @@ -96,9 +97,10 @@ fun SettingsView(settingsNav: SettingsNav, viewModel: SettingsViewModel = viewMo }, onClick = settingsNav.onNavigateToTailnetLock) } - - Lists.ItemDivider() - Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions) + if (!AndroidTVUtil.isAndroidTV()){ + Lists.ItemDivider() + Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions) + } managedByOrganization.value?.let { Lists.ItemDivider() From 0bd4ef932b5b8a1df8874f0c9895321d70af14c5 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:24:33 -0700 Subject: [PATCH 64/91] android: bump OSS (#549) OSS and Version updated to 1.77.44-tc0a1ed86c-gcafb114ae0a Signed-off-by: kari-ts --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index d2b1ad6..d3bb148 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.77.12-ta8f9c0d6e-g753b8d3fb4b" + versionName "1.77.44-tc0a1ed86c-gcafb114ae0a" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/go.mod b/go.mod index 9c44a3b..26baf59 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a + tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5 ) require ( diff --git a/go.sum b/go.sum index f674216..729efe5 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a h1:nGLKfmPOA8dmMqGUjBDIaUD4RkXo+QpP3TXoAKVwvHU= -tailscale.com v1.75.0-pre.0.20241014151013-a8f9c0d6e40a/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5 h1:L+jYpglYLtLNGAsHpcC50hJUZ/x0B2Axj2qEvbb7Xfc= +tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From c7b1362451a434da78878f09d16ae72fc5ceadae Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:05:44 -0700 Subject: [PATCH 65/91] android: use native search (#547) -Add dynamic suggestions -Use search bar with expanded view showing suggestions -dpad: only open keyboard when clicked on and not on scroll Updates tailscale/corp#18973 Fixes tailscale/corp#19231 Signed-off-by: kari-ts --- android/build.gradle | 1 + .../com/tailscale/ipn/ui/view/MainView.kt | 348 ++++++++++-------- .../ipn/ui/viewModel/MainViewModel.kt | 19 +- .../tailcale/ipn/ui/util/.TimeUtilTest.kt.swp | Bin 0 -> 12288 bytes 4 files changed, 219 insertions(+), 149 deletions(-) create mode 100644 android/src/test/kotlin/com/tailcale/ipn/ui/util/.TimeUtilTest.kt.swp diff --git a/android/build.gradle b/android/build.gradle index d3bb148..90ead1a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -134,6 +134,7 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" implementation "androidx.core:core-splashscreen:1.1.0-rc01" + implementation "androidx.compose.animation:animation:1.7.4" // Navigation dependencies. def nav_version = "2.8.2" diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 6966383..4bc8188 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -22,13 +22,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.ArrowDropDown -import androidx.compose.material.icons.outlined.Clear -import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu @@ -40,25 +41,23 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -86,7 +85,6 @@ import com.tailscale.ipn.ui.theme.exitNodeToggleButton import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.minTextSize import com.tailscale.ipn.ui.theme.primaryListItem -import com.tailscale.ipn.ui.theme.searchBarColors import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.short import com.tailscale.ipn.ui.theme.surfaceContainerListItem @@ -526,154 +524,119 @@ fun PeerList( remember { derivedStateOf { searchTermStr.isNotEmpty() && peerList.isEmpty() } }.value val netmap = viewModel.netmap.collectAsState() - val focusManager = LocalFocusManager.current var isFocussed by remember { mutableStateOf(false) } - var isListFocussed by remember { mutableStateOf(false) } - val expandedPeer = viewModel.expandedMenuPeer.collectAsState() val localClipboardManager = LocalClipboardManager.current - val enableSearch = !isAndroidTV() - if (enableSearch) { - Box(modifier = Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) { - OutlinedTextField( - modifier = - Modifier.fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 0.dp) - .onFocusChanged { isFocussed = it.isFocused }, - singleLine = true, - shape = MaterialTheme.shapes.extraLarge, - colors = MaterialTheme.colorScheme.searchBarColors, - leadingIcon = { - Icon(imageVector = Icons.Outlined.Search, contentDescription = "search") - }, - trailingIcon = { - if (isFocussed) { - IconButton( - onClick = { - focusManager.clearFocus() - onSearch("") - }) { - Icon( - imageVector = - if (searchTermStr.isEmpty()) Icons.Outlined.Close - else Icons.Outlined.Clear, - contentDescription = "clear search", - tint = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - }, - placeholder = { - Text( - text = stringResource(id = R.string.search), - style = MaterialTheme.typography.bodyLarge, - maxLines = 1) - }, - value = searchTermStr, - onValueChange = { onSearch(it) }) + Column(modifier = Modifier.fillMaxSize()) { + if (enableSearch) { + SearchWithDynamicSuggestions(viewModel, onSearch) + + Spacer(modifier = Modifier.height(if (showNoResults) 0.dp else 8.dp)) } - } - LazyColumn( - modifier = - Modifier.fillMaxSize() - .onFocusChanged { isListFocussed = it.isFocused } - .background(color = MaterialTheme.colorScheme.surface)) { - if (showNoResults) { - item { - Spacer( - Modifier.height(16.dp) - .fillMaxSize() - .focusable(false) - .background(color = MaterialTheme.colorScheme.surface)) - - Lists.LargeTitle( - stringResource(id = R.string.no_results), - bottomPadding = 8.dp, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Light) + // Peers display + LazyColumn( + modifier = + Modifier.fillMaxWidth() + .weight(1f) // LazyColumn gets the remaining vertical space + .onFocusChanged { isListFocussed = it.isFocused } + .background(color = MaterialTheme.colorScheme.surface)) { + + // Handle case when no results are found + if (showNoResults) { + item { + Spacer( + Modifier.height(16.dp) + .fillMaxSize() + .focusable(false) + .background(color = MaterialTheme.colorScheme.surface)) + Lists.LargeTitle( + stringResource(id = R.string.no_results), + bottomPadding = 8.dp, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light) + } } - } - var first = true - peerList.forEach { peerSet -> - if (!first) { - item(key = "user_divider_${peerSet.user?.ID ?: 0L}") { Lists.ItemDivider() } - } - first = false + // Iterate over peer sets to display them + var first = true + peerList.forEach { peerSet -> + if (!first) { + item(key = "user_divider_${peerSet.user?.ID ?: 0L}") { Lists.ItemDivider() } + } + first = false - // Sticky headers are a bit broken on Android TV - they hide their content - if (isAndroidTV()) { - item { NodesSectionHeader(peerSet = peerSet) } - } else { - stickyHeader { NodesSectionHeader(peerSet = peerSet) } - } + if (isAndroidTV()) { + item { NodesSectionHeader(peerSet = peerSet) } + } else { + stickyHeader { NodesSectionHeader(peerSet = peerSet) } + } - itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> - ListItem( - modifier = - Modifier.combinedClickable( - onClick = { onNavigateToPeerDetails(peer) }, - onLongClick = { viewModel.expandedMenuPeer.set(peer) }), - colors = MaterialTheme.colorScheme.listItem, - headlineContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier.padding(top = 2.dp) - .size(10.dp) - .background( - color = peer.connectedColor(netmap.value), - shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) - Text(text = peer.displayName, style = MaterialTheme.typography.titleMedium) - DropdownMenu( - expanded = expandedPeer.value?.StableID == peer.StableID, - onDismissRequest = { viewModel.hidePeerDropdownMenu() }) { - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.clipboard), - contentDescription = null) - }, - text = { Text(text = stringResource(R.string.copy_ip_address)) }, - onClick = { - viewModel.copyIpAddress(peer, localClipboardManager) - viewModel.hidePeerDropdownMenu() - }) - - netmap.value?.let { netMap -> - if (!peer.isSelfNode(netMap)) { - // Don't show the ping item for the self-node - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.timer), - contentDescription = null) - }, - text = { Text(text = stringResource(R.string.ping)) }, - onClick = { - viewModel.hidePeerDropdownMenu() - viewModel.startPing(peer) - }) + itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> + ListItem( + modifier = + Modifier.combinedClickable( + onClick = { onNavigateToPeerDetails(peer) }, + onLongClick = { viewModel.expandedMenuPeer.set(peer) }), + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.padding(top = 2.dp) + .size(10.dp) + .background( + color = peer.connectedColor(netmap.value), + shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text(text = peer.displayName, style = MaterialTheme.typography.titleMedium) + DropdownMenu( + expanded = expandedPeer.value?.StableID == peer.StableID, + onDismissRequest = { viewModel.hidePeerDropdownMenu() }) { + DropdownMenuItem( + leadingIcon = { + Icon( + painter = painterResource(R.drawable.clipboard), + contentDescription = null) + }, + text = { Text(text = stringResource(R.string.copy_ip_address)) }, + onClick = { + viewModel.copyIpAddress(peer, localClipboardManager) + viewModel.hidePeerDropdownMenu() + }) + netmap.value?.let { netMap -> + if (!peer.isSelfNode(netMap)) { + DropdownMenuItem( + leadingIcon = { + Icon( + painter = painterResource(R.drawable.timer), + contentDescription = null) + }, + text = { Text(text = stringResource(R.string.ping)) }, + onClick = { + viewModel.hidePeerDropdownMenu() + viewModel.startPing(peer) + }) + } } } - } - } - }, - supportingContent = { - Text( - text = peer.Addresses?.first()?.split("/")?.first() ?: "", - style = - MaterialTheme.typography.bodyMedium.copy( - lineHeight = MaterialTheme.typography.titleMedium.lineHeight)) - }) + } + }, + supportingContent = { + Text( + text = peer.Addresses?.first()?.split("/")?.first() ?: "", + style = + MaterialTheme.typography.bodyMedium.copy( + lineHeight = MaterialTheme.typography.titleMedium.lineHeight)) + }) + } } } - } + } } @Composable @@ -729,6 +692,103 @@ fun PromptPermissionsIfNecessary() { } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchWithDynamicSuggestions(viewModel: MainViewModel, onSearch: (String) -> Unit) { + val searchTerm by viewModel.searchTerm.collectAsState() + val filteredPeers by viewModel.peers.collectAsState() + var expanded by rememberSaveable { mutableStateOf(false) } + val netmap by viewModel.netmap.collectAsState() + + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + Column( + modifier = + Modifier.fillMaxWidth().focusRequester(focusRequester).clickable { + focusRequester.requestFocus() + keyboardController?.show() + }) { + SearchBar( + modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally), + inputField = { + SearchBarDefaults.InputField( + query = searchTerm, + onQueryChange = { query -> + viewModel.updateSearchTerm(query) + onSearch(query) + expanded = query.isNotEmpty() + }, + onSearch = { query -> + viewModel.updateSearchTerm(query) + onSearch(query) + expanded = false + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Search") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + trailingIcon = { + if (expanded) { + IconButton( + onClick = { + viewModel.updateSearchTerm("") + onSearch("") + expanded = false + focusManager.clearFocus() + keyboardController?.hide() + }) { + Icon(Icons.Default.Clear, contentDescription = "Clear search") + } + } + }) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + content = { + // Search results or suggestions + Column(Modifier.verticalScroll(rememberScrollState()).fillMaxSize()) { + filteredPeers.forEach { peerSet -> + val userName = peerSet.user?.DisplayName ?: "Unknown User" + peerSet.peers.forEach { peer -> + val deviceName = peer.displayName ?: "Unknown Device" + val ipAddress = peer.Addresses?.firstOrNull()?.split("/")?.first() ?: "No IP" + + ListItem( + headlineContent = { Text(userName) }, + supportingContent = { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + val onlineColor = peer.connectedColor(netmap) + Box( + modifier = + Modifier.size(10.dp) + .background(onlineColor, shape = RoundedCornerShape(50))) + Spacer(modifier = Modifier.size(8.dp)) + Text(deviceName) + } + Text(ipAddress) + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = + Modifier.clickable { + viewModel.updateSearchTerm(userName) + onSearch(userName) + expanded = false + focusManager.clearFocus() + keyboardController?.hide() + } + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp)) + } + } + } + }) + } +} + @Preview @Composable fun MainViewPreview() { diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index f1c7641..fdccab7 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -55,13 +55,15 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { private var vpnPermissionLauncher: ActivityResultLauncher? = null // The list of peers - val peers: StateFlow> = MutableStateFlow(emptyList()) + private val _peers = MutableStateFlow>(emptyList()) + val peers: StateFlow> = _peers // The current state of the IPN for determining view visibility val ipnState = Notifier.state // The active search term for filtering peers - val searchTerm: StateFlow = MutableStateFlow("") + private val _searchTerm = MutableStateFlow("") + val searchTerm: StateFlow = _searchTerm // True if we should render the key expiry bannder val showExpiry: StateFlow = MutableStateFlow(false) @@ -78,6 +80,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { // Icon displayed in the button to present the health view val healthIcon: StateFlow = MutableStateFlow(null) + fun updateSearchTerm(term: String) { + _searchTerm.value = term + } + fun hidePeerDropdownMenu() { expandedMenuPeer.set(null) } @@ -123,8 +129,9 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } viewModelScope.launch { - searchTerm.debounce(250L).collect { term -> - peers.set(peerCategorizer.groupedAndFilteredPeers(term)) + _searchTerm.debounce(250L).collect { term -> + val filteredPeers = peerCategorizer.groupedAndFilteredPeers(term) + _peers.value = filteredPeers } } @@ -132,7 +139,9 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { Notifier.netmap.collect { it -> it?.let { netmap -> peerCategorizer.regenerateGroupedPeers(netmap) - peers.set(peerCategorizer.groupedAndFilteredPeers(searchTerm.value)) + + // Immediately update _peers with the full peer list + _peers.value = peerCategorizer.groupedAndFilteredPeers(searchTerm.value) if (netmap.SelfNode.keyDoesNotExpire) { showExpiry.set(false) diff --git a/android/src/test/kotlin/com/tailcale/ipn/ui/util/.TimeUtilTest.kt.swp b/android/src/test/kotlin/com/tailcale/ipn/ui/util/.TimeUtilTest.kt.swp new file mode 100644 index 0000000000000000000000000000000000000000..ddc7a31371a99f9031df9f1cf8efe3d0e563894b GIT binary patch literal 12288 zcmeI2ON<*e7{}dK<pZzePI z+5Y_Z=g$+Zk5ulf&Cs2!Lh!kckgY%8+&r!KtbObqLfTx*fOqVZi+=8bT~wOk-lS6P z(yUf2P#zCflsTSOSTn;kk{4_valA;1a1WdkO!N*4S3PRonC%`k{Y48*{ z4i17zumzmO*t`ogP@n?>&<1wgByb<6fK%XKuE67j;*U}uMM9gq9*ubr<#Y9xiUXf& zVPYN9Tg1_>@Qm>5!c(Ci(WrmU6>0XQNV#WFwqWa85)ks^)XHU~>X4JF_oB6_u;+bo^C^nO%J|qpH-dwf9c1Afu8*xbOR<2qz zR9fuO@EH#AAZx8aj}dyO-M@uVJ71T5aS$w5CB!`_@OS%t&N)`;^{TA01bol}9RP>n z1f{;(r%X%QUbDkXEfF5h2lB{ZzuNtpce_H*D6d^$kqG^1)y(s-S2sE-0}sz#3}hQo z1H|f}qWtvA2FAy|071eQ_##I&nUs^5<){rHblY;98Kfv&s@!BK`r!@ z)`)zHuwN89LaJ>;3XWCWSQT=0U$EgbC$Z z^WOeOK0j97HnzP|snFZ#_R7{B$(J45wG#oVk$0SPUPJA|_)54Rh5_a%?3>KRGTX$6hXtt}I{J>9`j$-kas7 zmA@=gbX!!d#90E#{HlfMy7cFx`B0kZ@;TNS;n>V*$L8ut5^P8gKolx1n-V!O%`0|@ z%Vl~%M8eRd9OVroG!@)yCCQW;MfoCj%+g5uf{I21GbYM11lLn)DNNOwuq6kn^<(XeTsQ3H(Utbd@&Lt|-i4^EAtHUG_xjCbNSqjbe8A?b%*m?D=519A-43 zn{h{kW`-NDg@n!qYvcLW3o#^O$!kFBqIINyH`R2-{fXrLiS=Kj?CZkH3NJ9}uQ5Z6 zk|~l_kFDlxu+TH?HD`9CIqQe?u+h4Na#@o@>XvEs={|OhP?vjc-ozY89kF}f`+oFo zVS?mPpPhQ3G%Y<5Mxs>np|5O6p~vZDeX2B8+8yvX64r3H>h_RcT88Gm0-ejY+*2&H PbPJuBpWD0tK%M*nzkc`> literal 0 HcmV?d00001 From e7325f7d5f7a91b4518480ba01b10bb0f5e28dc7 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Fri, 18 Oct 2024 13:02:53 -0700 Subject: [PATCH 66/91] Makefile,*: use tailscale.com/cmd/mkversion We've suffered misalignment in versioning and toolchain usage due to the shell invocations downstream of ./version/tailscale-version.sh, but also the whole version data scheme in the Makefile was quite complicated, and required synchronization in the build.grade. - Makfile no longer needs to be version aware itself. - A Makefile target tailscale.version refreshes a local cached output from tailscale.com/cmd/mkversion which is updated when go.mod / go.sum change. - build.gradle loads tailscale.version to get the version string. - ldflags are produced from tailscale.version via version-ldflags.sh Updates tailscale/tailscale#13850 Signed-off-by: James Tucker --- .gitignore | 2 ++ Makefile | 55 +++++++++++++++--------------------- android/build.gradle | 12 +++++++- version-ldflags.sh | 10 +++++++ version/tailscale-version.sh | 46 ------------------------------ 5 files changed, 45 insertions(+), 80 deletions(-) create mode 100755 version-ldflags.sh delete mode 100755 version/tailscale-version.sh diff --git a/.gitignore b/.gitignore index aab9616..dd0dde5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ tailscale.jks libtailscale.aar libtailscale-sources.jar .DS_Store + +tailscale.version diff --git a/Makefile b/Makefile index 554d6d3..7b2a297 100644 --- a/Makefile +++ b/Makefile @@ -17,21 +17,7 @@ DEBUG_APK=tailscale-debug.apk RELEASE_AAB=tailscale-release.aab RELEASE_TV_AAB=tailscale-tv-release.aab LIBTAILSCALE=android/libs/libtailscale.aar -TAILSCALE_VERSION=$(shell ./version/tailscale-version.sh 200) -OUR_VERSION=$(shell git describe --dirty --exclude "*" --always --abbrev=200) -TAILSCALE_VERSION_ABBREV=$(shell ./version/tailscale-version.sh 11) -OUR_VERSION_ABBREV=$(shell git describe --exclude "*" --always --abbrev=11) -VERSION_LONG=$(TAILSCALE_VERSION_ABBREV)-g$(OUR_VERSION_ABBREV) -# Extract the long version build.gradle's versionName and strip quotes. -VERSIONNAME=$(patsubst "%",%,$(lastword $(shell grep versionName android/build.gradle))) -# Extract the x.y.z part for the short version. -VERSIONNAME_SHORT=$(shell echo $(VERSIONNAME) | cut -d - -f 1) -TAILSCALE_COMMIT=$(shell echo $(TAILSCALE_VERSION) | cut -d - -f 2 | cut -d t -f 2) # Extract the version code from build.gradle. -VERSIONCODE=$(lastword $(shell grep versionCode android/build.gradle)) -VERSIONCODE_PLUSONE=$(shell expr $(VERSIONCODE) + 1) -VERSION_LDFLAGS=-X tailscale.com/version.longStamp=$(VERSIONNAME) -X tailscale.com/version.shortStamp=$(VERSIONNAME_SHORT) -X tailscale.com/version.gitCommitStamp=$(TAILSCALE_COMMIT) -X tailscale.com/version.extraGitCommitStamp=$(OUR_VERSION) -FULL_LDFLAGS=$(VERSION_LDFLAGS) -w ifeq ($(shell uname),Linux) ANDROID_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip" ANDROID_TOOLS_SUM="bd1aa17c7ef10066949c88dc6c9c8d536be27f992a1f3b5a584f9bd2ba5646a0 commandlinetools-linux-9477386_latest.zip" @@ -111,17 +97,17 @@ tailscale-debug: $(DEBUG_APK) ## Build the debug APK # Builds the release AAB and signs it (phone/tablet/chromeOS variant) .PHONY: release -release: update-version jarsign-env $(RELEASE_AAB) ## Build the release AAB +release: jarsign-env $(RELEASE_AAB) ## Build the release AAB @jarsigner -sigalg SHA256withRSA -digestalg SHA-256 -keystore $(JKS_PATH) -storepass $(JKS_PASSWORD) $(RELEASE_AAB) tailscale # Builds the release AAB and signs it (androidTV variant) .PHONY: release-tv -release-tv: update-version jarsign-env $(RELEASE_TV_AAB) ## Build the release AAB +release-tv: jarsign-env $(RELEASE_TV_AAB) ## Build the release AAB @jarsigner -sigalg SHA256withRSA -digestalg SHA-256 -keystore $(JKS_PATH) -storepass $(JKS_PASSWORD) $(RELEASE_TV_AAB) tailscale # gradle-dependencies groups together the android sources and libtailscale needed to assemble tests/debug/release builds. .PHONY: gradle-dependencies -gradle-dependencies: $(shell find android -type f -not -path "android/build/*" -not -path '*/.*') $(LIBTAILSCALE) +gradle-dependencies: $(shell find android -type f -not -path "android/build/*" -not -path '*/.*') $(LIBTAILSCALE) tailscale.version $(DEBUG_APK): gradle-dependencies (cd android && ./gradlew test assembleDebug) @@ -141,6 +127,13 @@ tailscale-test.apk: gradle-dependencies (cd android && ./gradlew assembleApplicationTestAndroidTest) install -C ./android/build/outputs/apk/androidTest/applicationTest/android-applicationTest-androidTest.apk $@ +tailscale.version: go.mod go.sum $(wildcard .git/HEAD) + $(shell ./tool/go run tailscale.com/cmd/mkversion > tailscale.version) + +.PHONY: version +version: tailscale.version ## print the current version information + cat tailscale.version + # # Go Builds: # @@ -154,10 +147,10 @@ $(GOBIN)/gomobile: $(GOBIN)/gobind go.mod go.sum $(GOBIN)/gobind: go.mod go.sum ./tool/go install golang.org/x/mobile/cmd/gobind -$(LIBTAILSCALE): Makefile android/libs $(shell find libtailscale -name *.go) go.mod go.sum $(GOBIN)/gomobile +$(LIBTAILSCALE): Makefile android/libs $(shell find libtailscale -name *.go) go.mod go.sum $(GOBIN)/gomobile tailscale.version $(GOBIN)/gomobile bind -target android -androidapi 26 \ -tags "$$(./build-tags.sh)" \ - -ldflags "$(FULL_LDFLAGS)" \ + -ldflags "-w $$(./version-ldflags.sh)" \ -o $@ ./libtailscale .PHONY: libtailscale @@ -202,29 +195,25 @@ androidpath: @echo 'export PATH=$(ANDROID_HOME)/cmdline-tools/latest/bin:$(ANDROID_HOME)/platform-tools:$$PATH' .PHONY: tag_release -tag_release: ## Tag the current commit with the current version - git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" +tag_release: tailscale.version ## Tag the current commit with the current version + source tailscale.version && git tag -a "$${VERSION_LONG}" -m "OSS and Version updated to $${VERSION_LONG}" .PHONY: bumposs ## Bump to the latest oss and update the versions. -bumposs: update-oss update-version - git commit -sm "android: bump OSS" -m "OSS and Version updated to ${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum - git tag -a "$(VERSION_LONG)" -m "OSS and Version updated to ${VERSION_LONG}" +bumposs: update-oss tailscale.version + source tailscale.version && git commit -sm "android: bump OSS" -m "OSS and Version updated to $${VERSION_LONG}" go.toolchain.rev android/build.gradle go.mod go.sum + source tailscale.version && git tag -a "$${VERSION_LONG}" -m "OSS and Version updated to $${VERSION_LONG}" .PHONY: bump_version_code bump_version_code: ## Bump the version code in build.gradle - sed -i'.bak' 's/versionCode .*/versionCode $(VERSIONCODE_PLUSONE)/' android/build.gradle && rm android/build.gradle.bak - -.PHONY: update-version -update-version: ## Update the version in build.gradle - sed -i'.bak' 's/versionName .*/versionName "$(VERSION_LONG)"/' android/build.gradle && rm android/build.gradle.bak + sed -i'.bak' "s/versionCode .*/versionCode $$(expr $$(awk '/versionCode ([0-9]+)/{print $$2}' android/build.gradle) + 1)/" android/build.gradle && rm android/build.gradle.bak .PHONY: update-oss -update-oss: ## Update the tailscale.com go module and update the version in build.gradle +update-oss: ## Update the tailscale.com go module GOPROXY=direct ./tool/go get tailscale.com@main + ./tool/go mod tidy -compat=1.23 ./tool/go run tailscale.com/cmd/printdep --go > go.toolchain.rev.new mv go.toolchain.rev.new go.toolchain.rev - ./tool/go mod tidy -compat=1.23 # Get the commandline tools package, this provides (among other things) the sdkmanager binary. $(ANDROID_HOME)/cmdline-tools/latest/bin/sdkmanager: @@ -310,13 +299,13 @@ docker-remove-shell-image: ## Removes all docker shell image docker rmi --force tailscale-android-shell-amd64 .PHONY: clean -clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. -clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. +clean: clean-tailscale.version ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. @echo "Cleaning up old build artifacts" -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab @echo "Cleaning cached toolchain" -rm -rf $(HOME)/.cache/tailscale-go{,.extracted} -pkill -f gradle + -rm tailscale.version .PHONY: help help: ## Show this help diff --git a/android/build.gradle b/android/build.gradle index 90ead1a..65bd47a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 26 targetSdkVersion 34 versionCode 241 - versionName "1.77.44-tc0a1ed86c-gcafb114ae0a" + versionName getVersionProperty("VERSION_LONG") // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the @@ -182,3 +182,13 @@ def getLocalProperty(key, defaultValue) { return defaultValue } } + + +def getVersionProperty(key) { + // tailscale.version is created / updated by the makefile, it is in a loosely + // Makfile/envfile format, which is also loosely a properties file format. + // make tailscale.version + def versionProps = new Properties() + versionProps.load(project.file('../tailscale.version').newDataInputStream()) + return versionProps.getProperty(key).replaceAll('^\"|\"$', '') +} diff --git a/version-ldflags.sh b/version-ldflags.sh new file mode 100755 index 0000000..56f006a --- /dev/null +++ b/version-ldflags.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +source tailscale.version || echo >&2 "no tailscale.version file found" +if [[ -z "${VERSION_LONG}" ]]; then + exit 1 +fi +echo "-X tailscale.com/version.longStamp=${VERSION_LONG}" +echo "-X tailscale.com/version.shortStamp=${VERSION_SHORT}" +echo "-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" +echo "-X tailscale.com/version.extraGitCommitStamp=${VERSION_EXTRA_HASH}" diff --git a/version/tailscale-version.sh b/version/tailscale-version.sh deleted file mode 100755 index adc01dc..0000000 --- a/version/tailscale-version.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -# Print the version tailscale repository corresponding -# to the version listed in go.mod. - -set -euo pipefail - -# use the go toolchain from the tailscale.com -export TS_USE_TOOLCHAIN=1 - -go_list=$(go list -m tailscale.com) -# go list outputs `tailscale.com `. Extract the version. -mod_version=${go_list#tailscale.com} - -if [ -z "$mod_version" ]; then - echo >&2 "no version reported by go list -m tailscale.com: $go_list" - exit 1 -fi - -case "$mod_version" in - *-*-*) - # A pseudo-version such as "v1.1.1-0.20201030135043-eab6e9ea4e45" - # includes the commit hash. - mod_version=${mod_version##*-*-} - ;; -esac - -tailscale_clone=$(mktemp -d -t tailscale-clone-XXXXXXXXXX) -git clone -q https://github.com/tailscale/tailscale.git "$tailscale_clone" - -cd $tailscale_clone -git reset --hard -q -git clean -d -x -f -git fetch -q --all --tags -git checkout -q ${mod_version} - -eval $(./build_dist.sh shellvars) -git_hash=$(git rev-parse HEAD) -short_hash=$(echo "$git_hash" | cut -c1-9) -echo ${VERSION_SHORT}-t${short_hash} -cd /tmp -rm -rf "$tailscale_clone" From c1ef8b5f20ace1885e679b0a7f32a2f9b71013df Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Thu, 31 Oct 2024 12:23:37 -0700 Subject: [PATCH 67/91] android: bump OSS to 1.77.65-t698536947-ge7325f7d5 (#552) android: bump OSS OSS and Version updated to 1.77.65-t698536947-ge7325f7d5 Signed-off-by: Andrea Gottardo --- android/build.gradle | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 65bd47a..a311789 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -37,7 +37,7 @@ android { defaultConfig { minSdkVersion 26 targetSdkVersion 34 - versionCode 241 + versionCode 242 versionName getVersionProperty("VERSION_LONG") // This setting, which defaults to 'true', will cause Tailscale to fall diff --git a/go.mod b/go.mod index 26baf59..a1bd4de 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5 + tailscale.com v1.75.0-pre.0.20241031190034-6985369479db ) require ( diff --git a/go.sum b/go.sum index 729efe5..6b720bf 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5 h1:L+jYpglYLtLNGAsHpcC50hJUZ/x0B2Axj2qEvbb7Xfc= -tailscale.com v1.75.0-pre.0.20241028194956-c0a1ed86cbe5/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241031190034-6985369479db h1:fT4BHJAQceSiCsZbH94+u53RA7KhJFdhj0z/a15qgpo= +tailscale.com v1.75.0-pre.0.20241031190034-6985369479db/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From e89c259749c3d0e6b1741a849ca87a4823fb5195 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Thu, 31 Oct 2024 12:38:28 -0700 Subject: [PATCH 68/91] Makefile: fix clean action dependencies The explicit target was removed during patch production, but the dependency wasn't removed from the clean action. Updates #546 Updates tailscale/tailscale#13850 Signed-off-by: James Tucker --- .github/workflows/android.yml | 33 ++++++++++++++++++--------------- Makefile | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index aa3152c..f7ec869 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -6,7 +6,7 @@ on: - main pull_request: branches: - - '*' + - "*" jobs: build: @@ -15,20 +15,23 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: + - name: Check out code + uses: actions/checkout@v3 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Switch to Java 17 # Note: 17 is pre-installed on ubuntu-latest + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" - - name: Check out code - uses: actions/checkout@v3 - - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - name: Switch to Java 17 # Note: 17 is pre-installed on ubuntu-latest - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' + # Clean should essentially be a no-op, but make sure that it works. + - name: Clean + run: make clean - - name: Build APKs - run: make tailscale-debug.apk + - name: Build APKs + run: make tailscale-debug.apk - - name: Run tests - run: make test \ No newline at end of file + - name: Run tests + run: make test diff --git a/Makefile b/Makefile index 7b2a297..b1f2c5d 100644 --- a/Makefile +++ b/Makefile @@ -299,7 +299,7 @@ docker-remove-shell-image: ## Removes all docker shell image docker rmi --force tailscale-android-shell-amd64 .PHONY: clean -clean: clean-tailscale.version ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. +clean: ## Remove build artifacts. Does not purge docker build envs. Use dockerRemoveEnv for that. @echo "Cleaning up old build artifacts" -rm -rf android/build $(DEBUG_APK) $(RELEASE_AAB) $(RELEASE_TV_AAB) $(LIBTAILSCALE) android/libs *.apk *.aab @echo "Cleaning cached toolchain" From ba306bf883acbd526a8b9f324d4037dc3459b3e9 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:37:26 -0700 Subject: [PATCH 69/91] android: use a coroutine for loadfiles (#551) contentResolver.query is attempting to perform a network query on the main thread. Move this to a coroutine to prevent blocking. Fixes tailscale/corp#24293 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/ShareActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt index efd3be7..44a5837 100644 --- a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt @@ -14,14 +14,18 @@ import androidx.activity.compose.setContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.universalFit import com.tailscale.ipn.ui.view.TaildropView import com.tailscale.ipn.util.TSLog +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.random.Random // ShareActivity is the entry point for Taildrop share intents @@ -47,7 +51,7 @@ class ShareActivity : ComponentActivity() { super.onStart() // Ensure our app instance is initialized App.get() - loadFiles() + lifecycleScope.launch { withContext(Dispatchers.IO) { loadFiles() } } } override fun onNewIntent(intent: Intent) { From bd745b5254d8f4bc3dd1a9277ab40ee098b13b5e Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:55:37 -0800 Subject: [PATCH 70/91] android: fix avatar padding (#559) Update Avatar to take isFocusable as a parameter, allowing us to make the avatar focusable in the main view but not in the settings / user switcher view. This fixes the issue where the padding is too big in the settings / user switcher view. Fixes tailscale/corp#24370 Signed-off-by: kari-ts --- .../java/com/tailscale/ipn/ui/view/Avatar.kt | 95 +++++++++---------- .../com/tailscale/ipn/ui/view/MainView.kt | 11 ++- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index baae957..6f2a1c3 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -34,57 +34,52 @@ import com.tailscale.ipn.ui.model.IpnLocal @OptIn(ExperimentalCoilApi::class) @Composable -fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) { - var isFocused = remember { mutableStateOf(false) } - val focusManager = LocalFocusManager.current +fun Avatar( + profile: IpnLocal.LoginProfile?, + size: Int = 50, + action: (() -> Unit)? = null, + isFocusable: Boolean = false +) { + var isFocused = remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current - // Outer Box for the larger focusable and clickable area - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(4.dp) - .size((size * 1.5f).dp) // Focusable area is larger than the avatar - .clip(CircleShape) // Ensure both the focus and click area are circular - .background( - if (isFocused.value) MaterialTheme.colorScheme.surface - else Color.Transparent, - ) - .onFocusChanged { focusState -> - isFocused.value = focusState.isFocused - } - .focusable() // Make this outer Box focusable (after onFocusChanged) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = true), // Apply ripple effect inside circular bounds - onClick = { - action?.invoke() - focusManager.clearFocus() // Clear focus after clicking the avatar - } + // Determine the modifier based on whether the avatar is focusable + val outerModifier = + Modifier.then( + if (isFocusable) { + Modifier.padding(4.dp) + } else Modifier) // Add padding if focusable + .size((size * 1.5f).dp) + .clip(CircleShape) + .background(if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent) + .onFocusChanged { focusState -> isFocused.value = focusState.isFocused } + .then(if (isFocusable) Modifier.focusable() else Modifier) // Conditionally add focusable + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = true), + onClick = { + action?.invoke() + focusManager.clearFocus() // Clear focus after clicking + }) + + // Outer Box for the larger focusable and clickable area + Box(contentAlignment = Alignment.Center, modifier = outerModifier) { + // Inner Box to hold the avatar content (Icon or AsyncImage) + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) { + if (profile?.UserProfile?.ProfilePicURL != null) { + AsyncImage( + model = profile.UserProfile.ProfilePicURL, + modifier = Modifier.size(size.dp).clip(CircleShape), + contentDescription = null) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(R.string.settings_title), + modifier = + Modifier.size((size * 0.8f).dp) + .clip(CircleShape) // Icon size slightly smaller than the Box ) - ) { - // Inner Box to hold the avatar content (Icon or AsyncImage) - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(size.dp) - .clip(CircleShape) - ) { - if (profile?.UserProfile?.ProfilePicURL != null) { - AsyncImage( - model = profile.UserProfile.ProfilePicURL, - modifier = Modifier.size(size.dp).clip(CircleShape), - contentDescription = null - ) - } else { - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(R.string.settings_title), - modifier = Modifier - .size((size * 0.8f).dp) - .clip(CircleShape) // Icon size slightly smaller than the Box - ) - } - } + } } + } } - diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 4bc8188..419ccda 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -45,8 +45,15 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -188,7 +195,7 @@ fun MainView( when (user) { null -> SettingsButton { navigation.onNavigateToSettings() } else -> { - Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() } + Avatar(profile = user, size = 36, { navigation.onNavigateToSettings() }, isFocusable=true) } } } From 18ca09d0f389cc247df8e83c74d3a263c1637c66 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:29:36 -0800 Subject: [PATCH 71/91] android: fix MainActivityTest (#550) -Permissions are shown after 'Get Started' screen, fix ordering in test -Tap 'Authorize Tailscale' -Re-add instrumentation test runner in build.gradle Updates tailscale/corp#24242 Signed-off-by: kari-ts --- android/build.gradle | 1 + .../com/tailscale/ipn/MainActivityTest.kt | 29 ++++++++++--------- .../src/main/java/com/tailscale/ipn/App.kt | 23 +++++++++++++-- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index a311789..a1accd7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,6 +39,7 @@ android { targetSdkVersion 34 versionCode 242 versionName getVersionProperty("VERSION_LONG") + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // This setting, which defaults to 'true', will cause Tailscale to fall // back to the Google DNS servers if it cannot determine what the diff --git a/android/src/androidTest/kotlin/com/tailscale/ipn/MainActivityTest.kt b/android/src/androidTest/kotlin/com/tailscale/ipn/MainActivityTest.kt index 0a7fe5c..be76734 100644 --- a/android/src/androidTest/kotlin/com/tailscale/ipn/MainActivityTest.kt +++ b/android/src/androidTest/kotlin/com/tailscale/ipn/MainActivityTest.kt @@ -17,6 +17,11 @@ import androidx.test.uiautomator.UiSelector import dev.turingcomplete.kotlinonetimepassword.HmacAlgorithm import dev.turingcomplete.kotlinonetimepassword.TimeBasedOneTimePasswordConfig import dev.turingcomplete.kotlinonetimepassword.TimeBasedOneTimePasswordGenerator +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds import org.apache.commons.codec.binary.Base32 import org.junit.After import org.junit.Assert @@ -24,11 +29,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.net.HttpURLConnection -import java.net.URL -import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds @RunWith(AndroidJUnit4::class) @LargeTest @@ -59,18 +59,18 @@ class MainActivityTest { timeStep = 30, timeStepUnit = TimeUnit.SECONDS) val githubTOTP = TimeBasedOneTimePasswordGenerator(github2FASecret, config) - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - Log.d(TAG, "Wait for VPN permission prompt and accept") - device.find(By.text("Connection request")) - device.find(By.text("OK")).click() Log.d(TAG, "Click through Get Started screen") device.find(By.text("Get Started")) device.find(By.text("Get Started")).click() + Log.d(TAG, "Wait for VPN permission prompt and accept") + device.find(By.text("Connection request")) + device.find(By.text("OK")).click() + asNecessary( - timeout = 2.minutes, + 2.minutes, { Log.d(TAG, "Log in") device.find(By.text("Log in")).click() @@ -93,7 +93,6 @@ class MainActivityTest { }, { Log.d(TAG, "Make sure GitHub page has loaded") - device.find(By.text("New to GitHub")) device.find(By.text("Username or email address")) device.find(By.text("Sign in")) }, @@ -115,10 +114,15 @@ class MainActivityTest { .setText(githubTOTP.generate()) device.find(UiSelector().instance(0).className(Button::class.java)).click() }, + { + Log.d(TAG, "Authorizing Tailscale") + device.find(By.text("Authorize tailscale")).click() + }, { Log.d(TAG, "Accept Tailscale app") device.find(By.text("Learn more about OAuth")) // Sleep a little to give button time to activate + Thread.sleep(5.seconds.inWholeMilliseconds) device.find(UiSelector().instance(1).className(Button::class.java)).click() }, @@ -126,8 +130,7 @@ class MainActivityTest { Log.d(TAG, "Connect device") device.find(By.text("Connect device")) device.find(UiSelector().instance(0).className(Button::class.java)).click() - }, - ) + }) try { Log.d(TAG, "Accept Permission (Either Storage or Notifications)") diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 5126626..131a730 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -7,8 +7,10 @@ import android.app.Application import android.app.Notification import android.app.NotificationChannel import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.ConnectivityManager @@ -423,10 +425,27 @@ open class UninitializedApp : Application() { } } - // Calls stopVPN() followed by startVPN() to restart the VPN. fun restartVPN() { + // Register a receiver to listen for the completion of stopVPN + TSLog.d("KARI", "hi") + val stopReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + // Ensure stop intent is complete + if (intent?.action == IPNService.ACTION_STOP_VPN) { + // Unregister receiver after receiving the broadcast + context?.unregisterReceiver(this) + // Now start the VPN + startVPN() + } + } + } + + // Register the receiver before stopping VPN + val intentFilter = IntentFilter(IPNService.ACTION_STOP_VPN) + this.registerReceiver(stopReceiver, intentFilter) + stopVPN() - startVPN() } fun createNotificationChannel(id: String, name: String, description: String, importance: Int) { From 4c4148bd8ef43846d2d6875604f57e52065c2efc Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:32:55 -0800 Subject: [PATCH 72/91] android: fix issue where default avatar wasn't shown (#558) Always render the default icon first so that if the profile picture is not loaded or has an issue, the default is shown. Fixes tailscale/corp#24217 Signed-off-by: kari-ts --- .../java/com/tailscale/ipn/ui/view/Avatar.kt | 84 +++++++++++-------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index 6f2a1c3..a7d65bd 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -43,43 +43,53 @@ fun Avatar( var isFocused = remember { mutableStateOf(false) } val focusManager = LocalFocusManager.current - // Determine the modifier based on whether the avatar is focusable - val outerModifier = - Modifier.then( - if (isFocusable) { - Modifier.padding(4.dp) - } else Modifier) // Add padding if focusable - .size((size * 1.5f).dp) - .clip(CircleShape) - .background(if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent) - .onFocusChanged { focusState -> isFocused.value = focusState.isFocused } - .then(if (isFocusable) Modifier.focusable() else Modifier) // Conditionally add focusable - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = true), - onClick = { - action?.invoke() - focusManager.clearFocus() // Clear focus after clicking - }) - - // Outer Box for the larger focusable and clickable area - Box(contentAlignment = Alignment.Center, modifier = outerModifier) { - // Inner Box to hold the avatar content (Icon or AsyncImage) - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) { - if (profile?.UserProfile?.ProfilePicURL != null) { - AsyncImage( - model = profile.UserProfile.ProfilePicURL, - modifier = Modifier.size(size.dp).clip(CircleShape), - contentDescription = null) - } else { - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(R.string.settings_title), - modifier = - Modifier.size((size * 0.8f).dp) - .clip(CircleShape) // Icon size slightly smaller than the Box + // Outer Box for the larger focusable and clickable area + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(4.dp) + .size((size * 1.5f).dp) // Focusable area is larger than the avatar + .clip(CircleShape) // Ensure both the focus and click area are circular + .background( + if (isFocused.value) MaterialTheme.colorScheme.surface + else Color.Transparent, + ) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } + .focusable() // Make this outer Box focusable (after onFocusChanged) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = true), // Apply ripple effect inside circular bounds + onClick = { + action?.invoke() + focusManager.clearFocus() // Clear focus after clicking the avatar + } ) - } - } + ) { + // Inner Box to hold the avatar content (Icon or AsyncImage) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(size.dp) + .clip(CircleShape) + ) { + // Always display the default icon as a background layer + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(R.string.settings_title), + modifier = + Modifier.size((size * 0.8f).dp) + .clip(CircleShape) // Icon size slightly smaller than the Box + ) + + // Overlay the profile picture if available + profile?.UserProfile?.ProfilePicURL?.let { url -> + AsyncImage( + model = url, + modifier = Modifier.size(size.dp).clip(CircleShape), + contentDescription = null) + } + } } } From 08a062bfcfcca37111ed2fb488e340de847b1fe2 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:14:30 -0800 Subject: [PATCH 73/91] android: show hex code for TV log in (#557) Remove flag gating the display of a hex code for Android TV users, now that the change allowing hex code input in the admin console is merged. Fixes tailscale/tailscale#13277 Signed-off-by: kari-ts --- .../ipn/ui/viewModel/LoginQRViewModel.kt | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt index 7499fc0..36c1cbc 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/LoginQRViewModel.kt @@ -23,7 +23,6 @@ class LoginQRViewModel : IpnViewModel() { val numCode: StateFlow = MutableStateFlow(null) val qrCode: StateFlow = MutableStateFlow(null) // Remove this once changes to admin console allowing input code to be entered are made. - val codeEnabled = false init { viewModelScope.launch { @@ -31,17 +30,15 @@ class LoginQRViewModel : IpnViewModel() { url?.let { qrCode.set(generateQRCode(url, 200, 0)) - if (codeEnabled) { - // Extract the string after "https://login.tailscale.com/a/" - val prefix = "https://login.tailscale.com/a/" - val code = - if (it.startsWith(prefix)) { - it.removePrefix(prefix) - } else { - null - } - numCode.set(code) - } + // Extract the string after "https://login.tailscale.com/a/" + val prefix = "https://login.tailscale.com/a/" + val code = + if (it.startsWith(prefix)) { + it.removePrefix(prefix) + } else { + null + } + numCode.set(code) } ?: run { qrCode.set(null) From a10c4ef9da3c34565d5dee15ecb0b2267092dde3 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 19 Nov 2024 09:52:37 -0500 Subject: [PATCH 74/91] android/notifier: add ipn bus rate limit flag (#562) updates corp#24553 Adds the new flag to rate limit netmap updates on the ipnBus to one per 3 second interval. Signed-off-by: Jonathan Nobels --- .../com/tailscale/ipn/ui/notifier/Notifier.kt | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt index faf78c8..449f9de 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt @@ -3,7 +3,6 @@ package com.tailscale.ipn.ui.notifier -import android.util.Log import com.tailscale.ipn.App import com.tailscale.ipn.ui.model.Empty import com.tailscale.ipn.ui.model.Health @@ -11,6 +10,7 @@ import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn.Notify import com.tailscale.ipn.ui.model.Netmap import com.tailscale.ipn.ui.util.set +import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import com.tailscale.ipn.util.TSLog // Notifier is a wrapper around the IPN Bus notifier. It provides a way to watch // for changes in various parts of the Tailscale engine. You will typically only use @@ -69,23 +68,24 @@ object Notifier { NotifyWatchOpt.Netmap.value or NotifyWatchOpt.Prefs.value or NotifyWatchOpt.InitialState.value or - NotifyWatchOpt.InitialHealthState.value - manager = - app.watchNotifications(mask.toLong()) { notification -> - val notify = decoder.decodeFromStream(notification.inputStream()) - notify.State?.let { state.set(Ipn.State.fromInt(it)) } - notify.NetMap?.let(netmap::set) - notify.Prefs?.let(prefs::set) - notify.Engine?.let(engineStatus::set) - notify.TailFSShares?.let(tailFSShares::set) - notify.BrowseToURL?.let(browseToURL::set) - notify.LoginFinished?.let { loginFinished.set(it.property) } - notify.Version?.let(version::set) - notify.OutgoingFiles?.let(outgoingFiles::set) - notify.FilesWaiting?.let(filesWaiting::set) - notify.IncomingFiles?.let(incomingFiles::set) - notify.Health?.let(health::set) - } + NotifyWatchOpt.InitialHealthState.value or + NotifyWatchOpt.RateLimitNetmaps.value + manager = + app.watchNotifications(mask.toLong()) { notification -> + val notify = decoder.decodeFromStream(notification.inputStream()) + notify.State?.let { state.set(Ipn.State.fromInt(it)) } + notify.NetMap?.let(netmap::set) + notify.Prefs?.let(prefs::set) + notify.Engine?.let(engineStatus::set) + notify.TailFSShares?.let(tailFSShares::set) + notify.BrowseToURL?.let(browseToURL::set) + notify.LoginFinished?.let { loginFinished.set(it.property) } + notify.Version?.let(version::set) + notify.OutgoingFiles?.let(outgoingFiles::set) + notify.FilesWaiting?.let(filesWaiting::set) + notify.IncomingFiles?.let(incomingFiles::set) + notify.Health?.let(health::set) + } } } @@ -108,9 +108,10 @@ object Notifier { InitialTailFSShares(32), InitialOutgoingFiles(64), InitialHealthState(128), + RateLimitNetmaps(256), } fun setState(newState: Ipn.State) { _state.value = newState -} + } } From e95b7b7f62b6fd3c28ddeab58198bd3b67b8e791 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 19 Nov 2024 12:20:17 -0500 Subject: [PATCH 75/91] android: bump OSS (#564) OSS and Version updated to 1.77.113-t00517c818-g08a062bfc Signed-off-by: Jonathan Nobels --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- go.toolchain.rev | 2 +- libtailscale/backend.go | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index a1bd4de..9d84273 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/tailscale/tailscale-android go 1.23.1 require ( - github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc + github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241031190034-6985369479db + tailscale.com v1.75.0-pre.0.20241119130719-00517c818956 ) require ( @@ -52,7 +52,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect @@ -84,8 +84,8 @@ require ( golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index 6b720bf..1757e9f 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0 github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -156,8 +156,8 @@ github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVL github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= -github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc= -github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 h1:dmoPb3dG27tZgMtrvqfD/LW4w7gA6BSWl8prCPNmkCQ= +github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= @@ -206,8 +206,8 @@ golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -220,8 +220,8 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241031190034-6985369479db h1:fT4BHJAQceSiCsZbH94+u53RA7KhJFdhj0z/a15qgpo= -tailscale.com v1.75.0-pre.0.20241031190034-6985369479db/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.75.0-pre.0.20241119130719-00517c818956 h1:921sHsecXKeHcJXgUer07rl8WAV+u4nmLghUbQ6zeLk= +tailscale.com v1.75.0-pre.0.20241119130719-00517c818956/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= diff --git a/go.toolchain.rev b/go.toolchain.rev index 5d87594..500d853 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -bf15628b759344c6fc7763795a405ba65b8be5d7 +96578f73d04e1a231fa2a495ad3fa97747785bc6 diff --git a/libtailscale/backend.go b/libtailscale/backend.go index f9e923f..c0be4bc 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -294,7 +294,7 @@ func (a *App) newBackend(dataDir, directFileRoot string, appCtx AppContext, stor } sys.Set(engine) b.logIDPublic = logID.Public() - ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil) + ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper()) if err != nil { return nil, fmt.Errorf("netstack.Create: %w", err) } From 1538163b2d77cefdb49dee7827e94f46273291c1 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 19 Nov 2024 14:37:05 -0500 Subject: [PATCH 76/91] android: bump OSS (#565) OSS and Version updated to 1.77.113-t00517c818-ge95b7b7f6 Signed-off-by: Jonathan Nobels --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9d84273..8d90aad 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.75.0-pre.0.20241119130719-00517c818956 + tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76 ) require ( diff --git a/go.sum b/go.sum index 1757e9f..13f398f 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.75.0-pre.0.20241119130719-00517c818956 h1:921sHsecXKeHcJXgUer07rl8WAV+u4nmLghUbQ6zeLk= -tailscale.com v1.75.0-pre.0.20241119130719-00517c818956/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= +tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76 h1:0yp6sjCjsUUcBmLDor8jpVevFtvH31YNqkKeUvNX3b0= +tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= From f90967efae09bc38c9c1e0682c187ffbd54353d3 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 19 Nov 2024 15:10:40 -0500 Subject: [PATCH 77/91] Makefile: run version before attempting to build the apk/aab (#566) Makefile: run version before any APK builds updates tailscle/corp#24686 We must ensure the correct version file exists before kicking off any of the apk or aab builds. This also revs the docker image so we're picking up the latest toolchain. Signed-off-by: Jonathan Nobels --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b1f2c5d..2524860 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ # with this name, it will be used. # # The convention here is tailscale-android-build-amd64- -DOCKER_IMAGE=tailscale-android-build-amd64-120924 +DOCKER_IMAGE=tailscale-android-build-amd64-191124 export TS_USE_TOOLCHAIN=1 DEBUG_APK=tailscale-debug.apk @@ -109,21 +109,21 @@ release-tv: jarsign-env $(RELEASE_TV_AAB) ## Build the release AAB .PHONY: gradle-dependencies gradle-dependencies: $(shell find android -type f -not -path "android/build/*" -not -path '*/.*') $(LIBTAILSCALE) tailscale.version -$(DEBUG_APK): gradle-dependencies +$(DEBUG_APK): version gradle-dependencies (cd android && ./gradlew test assembleDebug) install -C android/build/outputs/apk/debug/android-debug.apk $@ -$(RELEASE_AAB): gradle-dependencies +$(RELEASE_AAB): version gradle-dependencies @echo "Building release AAB" (cd android && ./gradlew test bundleRelease) install -C ./android/build/outputs/bundle/release/android-release.aab $@ -$(RELEASE_TV_AAB): gradle-dependencies +$(RELEASE_TV_AAB): version gradle-dependencies @echo "Building TV release AAB" (cd android && ./gradlew test bundleRelease_tv) install -C ./android/build/outputs/bundle/release_tv/android-release_tv.aab $@ -tailscale-test.apk: gradle-dependencies +tailscale-test.apk: version gradle-dependencies (cd android && ./gradlew assembleApplicationTestAndroidTest) install -C ./android/build/outputs/apk/androidTest/applicationTest/android-applicationTest-androidTest.apk $@ From ca2d1615351ac7ee400942be252ddb2966d14266 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:09:54 -0800 Subject: [PATCH 78/91] android: exclude Adaptive Connectivity Services (#569) Default to excluding Adaptive Connectivity Services to fix issue where it is erroneously classifying wifi as broken Fixes tailscale/tailscale#14128 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/App.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 131a730..453a5f4 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -583,5 +583,7 @@ open class UninitializedApp : Application() { "com.vna.service.vvm", "com.dish.vvm", "com.comcast.modesto.vvm.client", + // Android Connectivity Service https://github.com/tailscale/tailscale/issues/14128 + "com.google.android.apps.scone", ) } From ed8a1b3573a06b45658946191ebe3d8f310852bc Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:12:14 -0800 Subject: [PATCH 79/91] android: initialize appInstance early (#561) Also log if get() is still being accessed before onCreate initializes appInstance so we can understand if this is still happening. Also remove a debug log that I forgot to delete. Updates tailscale/tailscale#14125 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/App.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 453a5f4..3e6af12 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -92,6 +92,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override fun onCreate() { super.onCreate() + appInstance = this + setUnprotectedInstance(this) createNotificationChannel( STATUS_CHANNEL_ID, getString(R.string.vpn_status), @@ -107,8 +109,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { getString(R.string.health_channel_name), getString(R.string.health_channel_description), NotificationManagerCompat.IMPORTANCE_HIGH) - appInstance = this - setUnprotectedInstance(this) } override fun onTerminate() { @@ -427,7 +427,6 @@ open class UninitializedApp : Application() { fun restartVPN() { // Register a receiver to listen for the completion of stopVPN - TSLog.d("KARI", "hi") val stopReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { From c56420bbc19166cf3dc0659b7e2de705270c751a Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:12:26 -0800 Subject: [PATCH 80/91] android: lazily init app in Client (#563) -Lazily init app in Client to ensure that we aren't trying to make any local API calls before app has been initialized. -Add @Volatile to ensure that isInitialized is always visible across threads and uses the updated value Updates tailscale/tailscale#14125 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/App.kt | 13 ++++++++++++- .../java/com/tailscale/ipn/ui/localapi/Client.kt | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 3e6af12..6427e91 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -90,6 +90,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { Log.d(s, s1) } + fun getLibtailscaleApp(): libtailscale.Application { + if (!isInitialized) { + initOnce() // Calls the synchronized initialization logic + } + return app + } + override fun onCreate() { super.onCreate() appInstance = this @@ -119,15 +126,19 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { viewModelStore.clear() } - private var isInitialized = false + @Volatile private var isInitialized = false @Synchronized private fun initOnce() { if (isInitialized) { return } + + initializeApp() isInitialized = true + } + private fun initializeApp() { val dataDir = this.filesDir.absolutePath // Set this to enable direct mode for taildrop whereby downloads will be saved directly diff --git a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt index 3ec004c..5b12338 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt @@ -13,6 +13,7 @@ import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.util.InputStreamAdapter import com.tailscale.ipn.util.TSLog +import com.tailscale.ipn.App import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -67,6 +68,11 @@ typealias PingResultHandler = (Result) -> Unit class Client(private val scope: CoroutineScope) { private val TAG = Client::class.simpleName + // Access libtailscale.Application lazily + private val app: libtailscale.Application by lazy { + App.get().getLibtailscaleApp() +} + fun start(options: Ipn.Options, responseHandler: (Result) -> Unit) { val body = Json.encodeToString(options).toByteArray() return post(Endpoint.START, body, responseHandler = responseHandler) From 788bb1dbcdd8ed2ee271a22aa05a49d978ffc070 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:12:49 -0800 Subject: [PATCH 81/91] android: handle null query results in ShareActivity (#567) If contentResolver.query returns null, or the URI is invalid, skip processing and log instead of crashing. Also, use 'use' for the cursor instead of 'let' to automatically close the cursor after processing. Fixes tailscale/corp#24293 Signed-off-by: kari-ts --- .../java/com/tailscale/ipn/ShareActivity.kt | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt index 44a5837..3e121f1 100644 --- a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt @@ -92,33 +92,25 @@ class ShareActivity : ComponentActivity() { } } - val pendingFiles: List = - uris?.filterNotNull()?.mapNotNull { - contentResolver?.query(it, null, null, null, null)?.let { c -> - val nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) - val sizeCol = c.getColumnIndex(OpenableColumns.SIZE) - c.moveToFirst() - val name: String = - c.getString(nameCol) - ?: run { - // For some reason, some content resolvers don't return a name. - // Try to build a name from a random integer plus file extension - // (if type can be determined), else just a random integer. - val rand = Random.nextLong() - contentResolver.getType(it)?.let { mimeType -> - MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { - extension -> - "$rand.$extension" - } ?: "$rand" - } ?: "$rand" + val pendingFiles: List = + uris?.filterNotNull()?.mapNotNull { uri -> + contentResolver?.query(uri, null, null, null, null)?.use { cursor -> + val nameCol = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val sizeCol = cursor.getColumnIndex(OpenableColumns.SIZE) + + if (cursor.moveToFirst()) { + val name: String = cursor.getString(nameCol) + ?: generateFallbackName(uri) + val size: Long = cursor.getLong(sizeCol) + Ipn.OutgoingFile(Name = name, DeclaredSize = size).apply { + this.uri = uri } - val size = c.getLong(sizeCol) - c.close() - val file = Ipn.OutgoingFile(Name = name, DeclaredSize = size) - file.uri = it - file - } - } ?: emptyList() + } else { + TSLog.e(TAG, "Cursor is empty for URI: $uri") + null + } + } + } ?: emptyList() if (pendingFiles.isEmpty()) { TSLog.e(TAG, "Share failure - no files extracted from intent") @@ -126,4 +118,11 @@ class ShareActivity : ComponentActivity() { requestedTransfers.set(pendingFiles) } + + private fun generateFallbackName(uri: Uri): String { + val randomId = Random.nextLong() + val mimeType = contentResolver?.getType(uri) + val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + return if (extension != null) "$randomId.$extension" else randomId.toString() +} } From 463c70df2a06f38ac2bffec595bdefef56ea2565 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 19 Nov 2024 19:25:43 -0800 Subject: [PATCH 82/91] android: modify proguard rules to not mangle Tailscale app symbols Updates tailscale/tailscale#14162 Signed-off-by: Brad Fitzpatrick --- android/proguard-rules.pro | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro index f95f0d7..566ece6 100644 --- a/android/proguard-rules.pro +++ b/android/proguard-rules.pro @@ -3,8 +3,11 @@ native ; } -# Keep the classes with syspolicy MDM keys, some of which -# get used only by the Go backend. +# Keep Tailcale classes for debuggability, but especially +# keep the classes with syspolicy MDM keys, some of which +# get used only by the Go backend. (The second rule is redundant, +# but explicit.) +-keep class com.tailscale.ipn.** { *; } -keep class com.tailscale.ipn.mdm.** { *; } # Keep specific classes from Tink library From d512aeffd12d1d41458d5451e221066a2acb3104 Mon Sep 17 00:00:00 2001 From: Nick Khyl <1761190+nickkhyl@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:56:29 -0600 Subject: [PATCH 83/91] mdm: update MDMSettings (and syspolicy) when application restrictions change (#571) In this PR, we update the Android app to register a broadcast receiver that listens for android.content.Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED and updates MDMSettings whenever a change occurs. This, in turn, notifies the Go backend and causes it to reload syspolicy, ensuring it reflects the updated MDM settings. Updates tailscale/tailscale#12687 Signed-off-by: Nick Khyl --- .../src/main/java/com/tailscale/ipn/App.kt | 8 +++++++ .../ipn/mdm/MDMSettingsChangedReceiver.kt | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 6427e91..fde6541 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.tailscale.ipn.mdm.MDMSettings +import com.tailscale.ipn.mdm.MDMSettingsChangedReceiver import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Request import com.tailscale.ipn.ui.model.Ipn @@ -71,6 +72,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { val dns = DnsConfig() private lateinit var connectivityManager: ConnectivityManager + private lateinit var mdmChangeReceiver: MDMSettingsChangedReceiver private lateinit var app: libtailscale.Application override val viewModelStore: ViewModelStore @@ -101,6 +103,11 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { super.onCreate() appInstance = this setUnprotectedInstance(this) + + mdmChangeReceiver = MDMSettingsChangedReceiver() + val filter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) + registerReceiver(mdmChangeReceiver, filter) + createNotificationChannel( STATUS_CHANNEL_ID, getString(R.string.vpn_status), @@ -124,6 +131,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { notificationManager.cancelAll() applicationScope.cancel() viewModelStore.clear() + unregisterReceiver(mdmChangeReceiver) } @Volatile private var isInitialized = false diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt new file mode 100644 index 0000000..b4d17b8 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt @@ -0,0 +1,21 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.mdm + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.RestrictionsManager +import com.tailscale.ipn.App +import com.tailscale.ipn.util.TSLog + +class MDMSettingsChangedReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == android.content.Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) { + TSLog.d("syspolicy", "MDM settings changed") + val restrictionsManager = context?.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + MDMSettings.update(App.get(), restrictionsManager) + } + } +} \ No newline at end of file From 0fe76a7d464b036f0bf721588a378f352d614575 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:30:15 -0800 Subject: [PATCH 84/91] android: synchronize Notifier app initialization (#568) @Synchronize Notifier.setApp and Notifier.start to make sure that app isn't being accessed while being set. Updates tailscale/corp#24694 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt index 449f9de..4962823 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/Notifier.kt @@ -52,11 +52,13 @@ object Notifier { private lateinit var app: libtailscale.Application private var manager: libtailscale.NotificationManager? = null + @Synchronized @JvmStatic fun setApp(newApp: libtailscale.Application) { app = newApp } + @Synchronized @OptIn(ExperimentalSerializationApi::class) fun start(scope: CoroutineScope) { TSLog.d(TAG, "Starting Notifier") From 91a13164521999e543e5095771d99d4839bcd527 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Mon, 25 Nov 2024 14:55:20 -0500 Subject: [PATCH 85/91] android: bump OSS (#572) OSS and Version updated to 1.77.114-tbb3d0cae5-g0fe76a7d4 Signed-off-by: Jonathan Nobels --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8d90aad..25c7e64 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76 + tailscale.com v1.77.0-pre.0.20241125164922-788121f47536 ) require ( diff --git a/go.sum b/go.sum index 13f398f..76a0df1 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76 h1:0yp6sjCjsUUcBmLDor8jpVevFtvH31YNqkKeUvNX3b0= -tailscale.com v1.77.0-pre.0.20241119172557-bb3d0cae5f76/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= +tailscale.com v1.77.0-pre.0.20241125164922-788121f47536 h1:n1W86Eeq8gtA8tRhggw1idumEImsP+Yv3AxDCjs6Po0= +tailscale.com v1.77.0-pre.0.20241125164922-788121f47536/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= From 61c7c3c8c1c279be76db0ee858399d59bfeca687 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Tue, 26 Nov 2024 10:05:05 -0800 Subject: [PATCH 86/91] HealthNotifier: fix dependent messages handling (#573) Fixes tailscale/corp#24582 Android port of tailscale/corp#24719. We were not clearing dependency warnings when a new warning was added which was listed as a dependency of a pre-existing warning. For instance, if `dns-read-os-config-failed` is added to the warnings before `network-status` is added, we were ignoring the dependency by not removing `dns-read-os-config-failed` upon adding `network-status`. This PR addresses that. Signed-off-by: Andrea Gottardo --- .../tailscale/ipn/ui/notifier/HealthNotifier.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt index 5193328..4ebac07 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt @@ -62,8 +62,21 @@ class HealthNotifier( val warningsBeforeAdd = currentWarnings.value val currentWarnableCodes = warnings.map { it.WarnableCode }.toSet() val addedWarnings: MutableSet = mutableSetOf() + val removedByNewDependency: MutableSet = mutableSetOf() val isWarmingUp = warnings.any { it.WarnableCode == "warming-up" } + /// Checks if there is any warning in `warningsBeforeAdd` that needs to be removed because the new warning `w` + /// is listed as a dependency of a warning already in `warningsBeforeAdd`, and removes it. + fun dropDependenciesForAddedWarning(w: UnhealthyState) { + for (warning in warningsBeforeAdd) { + warning.DependsOn?.let { + if (it.contains(w.WarnableCode)) { + removedByNewDependency.add(warning) + } + } + } + } + for (warning in warnings) { if (ignoredWarnableCodes.contains(warning.WarnableCode)) { continue @@ -81,6 +94,7 @@ class HealthNotifier( } else if (!isWarmingUp) { TSLog.d(TAG, "Adding health warning: ${warning.WarnableCode}") this.currentWarnings.set(this.currentWarnings.value + warning) + dropDependenciesForAddedWarning(warning) if (warning.Severity == Health.Severity.high) { this.sendNotification(warning.Title, warning.Text, warning.WarnableCode) } @@ -89,7 +103,7 @@ class HealthNotifier( } } - val warningsToDrop = warningsBeforeAdd.minus(addedWarnings) + val warningsToDrop = warningsBeforeAdd.minus(addedWarnings).union(removedByNewDependency) if (warningsToDrop.isNotEmpty()) { TSLog.d(TAG, "Dropping health warnings with codes $warningsToDrop") this.removeNotifications(warningsToDrop) From fda38205825eff685a2504ce2b080552e5ebc53f Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Tue, 26 Nov 2024 12:18:22 -0800 Subject: [PATCH 87/91] HealthNotifier: prevent and drop all warnings in the Stopped state (#575) Updates tailscale/tailscale#12960 When the client is Stopped after running, a false positive DERP warnings was getting presented. This was not happening on Apple platforms because we never leave the client in a Stopped state, the extension instantly terminates. Since that's not the case on Android, this PR ensures that: - we do not present any warnings when the client is Stopped (nothing should be broken when nothing is running) - if we enter the Stopped state, any pre-existing warnings generated while the client was running are dropped Signed-off-by: Andrea Gottardo --- .../src/main/java/com/tailscale/ipn/App.kt | 2 +- .../ipn/ui/notifier/HealthNotifier.kt | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index fde6541..8d8a076 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -158,7 +158,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { Request.setApp(app) Notifier.setApp(app) Notifier.start(applicationScope) - healthNotifier = HealthNotifier(Notifier.health, applicationScope) + healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope) connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns) initViewModels() diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt index 4ebac07..8386bcb 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt @@ -12,12 +12,14 @@ import com.tailscale.ipn.R import com.tailscale.ipn.UninitializedApp.Companion.notificationManager import com.tailscale.ipn.ui.model.Health import com.tailscale.ipn.ui.model.Health.UnhealthyState +import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.util.TSLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -25,6 +27,7 @@ import kotlinx.coroutines.launch @OptIn(FlowPreview::class) class HealthNotifier( healthStateFlow: StateFlow, + ipnStateFlow: StateFlow, scope: CoroutineScope, ) { companion object { @@ -45,11 +48,22 @@ class HealthNotifier( scope.launch { healthStateFlow .distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() } + .combine(ipnStateFlow, ::Pair) .debounce(5000) - .collect { health -> - TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") - health?.Warnings?.let { - notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray()) + .collect { pair -> + val health = pair.first + val ipnState = pair.second + // When the client is Stopped, no warnings should get added, and any warnings added + // previously should be removed. + if (ipnState == Ipn.State.Stopped) { + TSLog.d(TAG, "Ignoring and dropping all pre-existing health messages in the Stopped state") + dropAllWarnings() + return@collect + } else { + TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") + health?.Warnings?.let { + notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray()) + } } } } @@ -65,8 +79,11 @@ class HealthNotifier( val removedByNewDependency: MutableSet = mutableSetOf() val isWarmingUp = warnings.any { it.WarnableCode == "warming-up" } - /// Checks if there is any warning in `warningsBeforeAdd` that needs to be removed because the new warning `w` - /// is listed as a dependency of a warning already in `warningsBeforeAdd`, and removes it. + /** + * dropDependenciesForAddedWarning checks if there is any warning in `warningsBeforeAdd` that + * needs to be removed because the new warning `w` is listed as a dependency of a warning + * already in `warningsBeforeAdd`, and removes it. + */ fun dropDependenciesForAddedWarning(w: UnhealthyState) { for (warning in warningsBeforeAdd) { warning.DependsOn?.let { @@ -112,6 +129,14 @@ class HealthNotifier( this.updateIcon() } + /** + * Sets the icon displayed to represent the overall health state. + * + * - If there are any high severity warnings, or warnings that affect internet connectivity, + * a warning icon is displayed. + * - If there are any other kind of warnings, an info icon is displayed. + * - If there are no warnings at all, no icon is set. + */ private fun updateIcon() { if (currentWarnings.value.isEmpty()) { this.currentIcon.set(null) @@ -145,6 +170,16 @@ class HealthNotifier( notificationManager.notify(code.hashCode(), notification) } + /** + * Removes all warnings currently displayed, including any system notifications, and + * updates the icon (causing it to be set to null since the set of warnings is empty). + */ + private fun dropAllWarnings() { + removeNotifications(this.currentWarnings.value) + this.currentWarnings.set(emptySet()) + this.updateIcon() + } + private fun removeNotifications(warnings: Set) { TSLog.d(TAG, "Removing notifications for $warnings") for (warning in warnings) { From f35b3f92746ef4dfa064284a692a561c955b95d9 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 26 Nov 2024 17:25:35 -0500 Subject: [PATCH 88/91] android: move node search to background and fix avatar padding (#574) android: use background search and fix avatar padding fixes tailscale/corp#24847 fixes tailsacle/corp#24848 Search jobs are moved to the default dispatcher so they do not block the UI thread. The avatar boxing is now used only conditionally on AndroidTV. Signed-off-by: Jonathan Nobels --- .../src/main/java/com/tailscale/ipn/App.kt | 2 +- .../com/tailscale/ipn/ui/util/ModifierUtil.kt | 18 ++++ .../java/com/tailscale/ipn/ui/view/Avatar.kt | 86 +++++++++---------- .../ipn/ui/viewModel/MainViewModel.kt | 23 +++-- 4 files changed, 78 insertions(+), 51 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/util/ModifierUtil.kt diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 8d8a076..3b5b243 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -163,7 +163,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns) initViewModels() applicationScope.launch { - Notifier.state.collect { state -> + Notifier.state.collect { _ -> combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled -> Pair(state, forceEnabled) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ModifierUtil.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ModifierUtil.kt new file mode 100644 index 0000000..49d2f19 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ModifierUtil.kt @@ -0,0 +1,18 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.util + +import androidx.compose.ui.Modifier + +/// Applies different modifiers to the receiver based on a condition. +inline fun Modifier.conditional( + condition: Boolean, + ifTrue: Modifier.() -> Modifier, + ifFalse: Modifier.() -> Modifier = { this }, +): Modifier = + if (condition) { + then(ifTrue(Modifier)) + } else { + then(ifFalse(Modifier)) + } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index a7d65bd..89ef4d9 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -31,6 +31,8 @@ import coil.annotation.ExperimentalCoilApi import coil.compose.AsyncImage import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.IpnLocal +import com.tailscale.ipn.ui.util.AndroidTVUtil +import com.tailscale.ipn.ui.util.conditional @OptIn(ExperimentalCoilApi::class) @Composable @@ -43,53 +45,49 @@ fun Avatar( var isFocused = remember { mutableStateOf(false) } val focusManager = LocalFocusManager.current - // Outer Box for the larger focusable and clickable area - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(4.dp) - .size((size * 1.5f).dp) // Focusable area is larger than the avatar - .clip(CircleShape) // Ensure both the focus and click area are circular - .background( - if (isFocused.value) MaterialTheme.colorScheme.surface - else Color.Transparent, - ) - .onFocusChanged { focusState -> - isFocused.value = focusState.isFocused - } - .focusable() // Make this outer Box focusable (after onFocusChanged) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = true), // Apply ripple effect inside circular bounds - onClick = { - action?.invoke() + // Outer Box for the larger focusable and clickable area + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.conditional(AndroidTVUtil.isAndroidTV(), { padding(4.dp) }) + .conditional( + AndroidTVUtil.isAndroidTV(), + { + size((size * 1.5f).dp) // Focusable area is larger than the avatar + }) + .clip(CircleShape) // Ensure both the focus and click area are circular + .background( + if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent, + ) + .onFocusChanged { focusState -> isFocused.value = focusState.isFocused } + .focusable() // Make this outer Box focusable (after onFocusChanged) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = true), // Apply ripple effect inside circular bounds + onClick = { + action?.invoke() focusManager.clearFocus() // Clear focus after clicking the avatar - } - ) - ) { + })) { // Inner Box to hold the avatar content (Icon or AsyncImage) Box( contentAlignment = Alignment.Center, - modifier = Modifier - .size(size.dp) - .clip(CircleShape) - ) { - // Always display the default icon as a background layer - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(R.string.settings_title), - modifier = - Modifier.size((size * 0.8f).dp) - .clip(CircleShape) // Icon size slightly smaller than the Box - ) + modifier = Modifier.size(size.dp).clip(CircleShape)) { + // Always display the default icon as a background layer + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(R.string.settings_title), + modifier = + Modifier.conditional(AndroidTVUtil.isAndroidTV(), { size((size * 0.8f).dp) }) + .clip(CircleShape) // Icon size slightly smaller than the Box + ) - // Overlay the profile picture if available - profile?.UserProfile?.ProfilePicURL?.let { url -> - AsyncImage( - model = url, - modifier = Modifier.size(size.dp).clip(CircleShape), - contentDescription = null) - } - } - } + // Overlay the profile picture if available + profile?.UserProfile?.ProfilePicURL?.let { url -> + AsyncImage( + model = url, + modifier = Modifier.size(size.dp).clip(CircleShape), + contentDescription = null) + } + } + } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index fdccab7..fa92360 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -23,7 +23,9 @@ import com.tailscale.ipn.ui.util.PeerCategorizer import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.set +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -77,6 +79,8 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { val isVpnActive: StateFlow = vpnViewModel.vpnActive + var searchJob: Job? = null + // Icon displayed in the button to present the health view val healthIcon: StateFlow = MutableStateFlow(null) @@ -130,18 +134,25 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { viewModelScope.launch { _searchTerm.debounce(250L).collect { term -> - val filteredPeers = peerCategorizer.groupedAndFilteredPeers(term) - _peers.value = filteredPeers + // run the search as a background task + searchJob?.cancel() + searchJob = + launch(Dispatchers.Default) { + val filteredPeers = peerCategorizer.groupedAndFilteredPeers(term) + _peers.value = filteredPeers + } } } viewModelScope.launch { Notifier.netmap.collect { it -> it?.let { netmap -> - peerCategorizer.regenerateGroupedPeers(netmap) - - // Immediately update _peers with the full peer list - _peers.value = peerCategorizer.groupedAndFilteredPeers(searchTerm.value) + searchJob?.cancel() + launch(Dispatchers.Default) { + peerCategorizer.regenerateGroupedPeers(netmap) + val filteredPeers = peerCategorizer.groupedAndFilteredPeers(searchTerm.value) + _peers.value = filteredPeers + } if (netmap.SelfNode.keyDoesNotExpire) { showExpiry.set(false) From 7857f81f9fbaf61693f04456ed455a303db4aeb4 Mon Sep 17 00:00:00 2001 From: Keli <104461838+kelivel@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:16:20 -0800 Subject: [PATCH 89/91] android: update hex input code instructions for TV log in (#576) Updates the login UI to provide the input code location in the admin console. Fixes tailscale/corp#24837 Signed-off-by: Keli Velazquez --- android/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index da43495..59bbe9f 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -244,7 +244,7 @@ Tailscale is a mesh VPN for securely connecting your devices. All connections are device-to-device, so we never see your data. We collect and use your email address and name, as well as your device name, OS version, and IP address in order to help you to connect your devices and manage your settings. We log when you are connected to your network. Scan this QR code to log in to your tailnet - or enter this code in the admin console: %1$s + or enter this code in the Machines > Add device section of the admin console: \n%1$s VPN is not ready to start From 7fc51f5c5867b31119ff63aac8b018a5f8ae9c49 Mon Sep 17 00:00:00 2001 From: Keli <104461838+kelivel@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:00:19 -0800 Subject: [PATCH 90/91] android: bump OSS (#577) OSS and Version updated to 1.77.139-t788121f47-g7857f81f9 Signed-off-by: Keli Velazquez --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 25c7e64..7a09a28 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab inet.af/netaddr v0.0.0-20220617031823-097006376321 - tailscale.com v1.77.0-pre.0.20241125164922-788121f47536 + tailscale.com v1.77.0-pre.0.20241202172800-8d0c690f8997 ) require ( diff --git a/go.sum b/go.sum index 76a0df1..3e87841 100644 --- a/go.sum +++ b/go.sum @@ -256,5 +256,5 @@ inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQ inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.77.0-pre.0.20241125164922-788121f47536 h1:n1W86Eeq8gtA8tRhggw1idumEImsP+Yv3AxDCjs6Po0= -tailscale.com v1.77.0-pre.0.20241125164922-788121f47536/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= +tailscale.com v1.77.0-pre.0.20241202172800-8d0c690f8997 h1:jAL0TXstGYT1L0V2qH+zpQSEBwBGgIOVkflckrU0kqo= +tailscale.com v1.77.0-pre.0.20241202172800-8d0c690f8997/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= From f96b6328df347c16efb2b2cfa66aaec86e4985a6 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:41:44 -0800 Subject: [PATCH 91/91] android: emphasize hex code in TV login (#578) Add a rounded box around the hex code and emphasize the code with font/styling Updates tailscale/corp#24837 Signed-off-by: kari-ts --- .../com/tailscale/ipn/ui/view/LoginQRView.kt | 37 ++++++++++++++----- android/src/main/res/values/strings.xml | 2 +- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt index 03ea17e..022e471 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/LoginQRView.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -41,7 +43,6 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( Dialog(onDismissRequest = onDismiss) { val image by model.qrCode.collectAsState() val numCode by model.numCode.collectAsState() - Column( modifier = Modifier.clip(RoundedCornerShape(10.dp)) @@ -52,12 +53,13 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( Text( text = stringResource(R.string.scan_to_connect_to_your_tailnet), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface) + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center) + Box( modifier = Modifier.size(200.dp) - .background(MaterialTheme.colorScheme.onSurface) - .fillMaxWidth(), + .background(MaterialTheme.colorScheme.onSurface), contentAlignment = Alignment.Center) { image?.let { Image( @@ -66,13 +68,28 @@ fun LoginQRView(onDismiss: () -> Unit = {}, model: LoginQRViewModel = viewModel( modifier = Modifier.fillMaxSize()) } } - numCode?.let { it -> - Text( - text = stringResource(R.string.enter_code_to_connect_to_tailnet, it), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface) + Text( + text = stringResource(R.string.enter_code_to_connect_to_tailnet), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface) + + numCode?.let { + Box( + modifier = + Modifier + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center) { + Text( + text =it, + style = + MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface) + } + } + Button(onClick = onDismiss, modifier = Modifier.padding(top = 16.dp)) { + Text(text = stringResource(R.string.dismiss)) } - Button(onClick = onDismiss) { Text(text = stringResource(R.string.dismiss)) } } } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 59bbe9f..8d6657e 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -244,7 +244,7 @@ Tailscale is a mesh VPN for securely connecting your devices. All connections are device-to-device, so we never see your data. We collect and use your email address and name, as well as your device name, OS version, and IP address in order to help you to connect your devices and manage your settings. We log when you are connected to your network. Scan this QR code to log in to your tailnet - or enter this code in the Machines > Add device section of the admin console: \n%1$s + or enter this code in the Machines > Add device section of the admin console: VPN is not ready to start