diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index 6fde743..f0c05dd 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -74,7 +74,6 @@ import com.tailscale.ipn.mdm.BooleanSetting; import com.tailscale.ipn.mdm.MDMSettings; import com.tailscale.ipn.mdm.ShowHideSetting; import com.tailscale.ipn.mdm.StringSetting; -import com.tailscale.ipn.ui.service.IpnManager; import com.tailscale.ipn.ui.localapi.LocalApiClient; @@ -198,7 +197,7 @@ public class App extends Application { public boolean autoConnect = false; public boolean vpnReady = false; - void setTileReady(boolean ready) { + public void setTileReady(boolean ready) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return; } @@ -270,7 +269,7 @@ public class App extends Application { return getPackageManager().hasSystemFeature("android.hardware.type.pc"); } - void prepareVPN(Activity act, int reqCode) { + public void prepareVPN(Activity act, int reqCode) { act.runOnUiThread(new Runnable() { @Override public void run() { Intent intent = VpnService.prepare(act); diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.java b/android/src/main/java/com/tailscale/ipn/IPNService.java deleted file mode 100644 index ee5b491..0000000 --- a/android/src/main/java/com/tailscale/ipn/IPNService.java +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.util.Log; -import android.os.Build; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.VpnService; -import android.system.OsConstants; -import androidx.work.WorkManager; -import androidx.work.OneTimeWorkRequest; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import com.tailscale.ipn.ui.service.IpnManager; - -public class IPNService extends VpnService { - public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN"; - public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"; - private IpnManager ipnManager; - - @Override - public void onCreate(){ - super.onCreate(); - ipnManager = new IpnManager(); - } - - @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && ACTION_STOP_VPN.equals(intent.getAction())) { - ((App)getApplicationContext()).autoConnect = false; - close(); - return START_NOT_STICKY; - } - if (intent != null && "android.net.VpnService".equals(intent.getAction())) { - // Start VPN and connect to it due to Always-on VPN - Intent i = new Intent(IPNReceiver.INTENT_CONNECT_VPN); - i.setPackage(getPackageName()); - i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class); - sendBroadcast(i); - requestVPN(); - connect(); - return START_STICKY; - } - requestVPN(); - App app = ((App)getApplicationContext()); - if (app.vpnReady && app.autoConnect) { - ipnManager.connect(); - } - return START_STICKY; - } - - private void close() { - stopForeground(true); - disconnect(); - } - - @Override public void onDestroy() { - close(); - super.onDestroy(); - } - - @Override public void onRevoke() { - close(); - super.onRevoke(); - } - - private PendingIntent configIntent() { - return PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } - - private void disallowApp(VpnService.Builder b, String name) { - try { - b.addDisallowedApplication(name); - } catch (PackageManager.NameNotFoundException e) { - return; - } - } - - protected VpnService.Builder newBuilder() { - VpnService.Builder b = new VpnService.Builder() - .setConfigureIntent(configIntent()) - .allowFamily(OsConstants.AF_INET) - .allowFamily(OsConstants.AF_INET6); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - b.setMetered(false); // Inherit the metered status from the underlying networks. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - b.setUnderlyingNetworks(null); // Use all available networks. - - // RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 - this.disallowApp(b, "com.google.android.apps.messaging"); - - // Stadia https://github.com/tailscale/tailscale/issues/3460 - this.disallowApp(b, "com.google.stadia.android"); - - // Android Auto https://github.com/tailscale/tailscale/issues/3828 - this.disallowApp(b, "com.google.android.projection.gearhead"); - - // GoPro https://github.com/tailscale/tailscale/issues/2554 - this.disallowApp(b, "com.gopro.smarty"); - - // Sonos https://github.com/tailscale/tailscale/issues/2548 - this.disallowApp(b, "com.sonos.acr"); - this.disallowApp(b, "com.sonos.acr2"); - - // Google Chromecast https://github.com/tailscale/tailscale/issues/3636 - this.disallowApp(b, "com.google.android.apps.chromecast.app"); - - return b; - } - - public void notify(String title, String message) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(title) - .setContentText(message) - .setContentIntent(configIntent()) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT); - - NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build()); - } - - public void updateStatusNotification(String title, String message) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(title) - .setContentText(message) - .setContentIntent(configIntent()) - .setPriority(NotificationCompat.PRIORITY_LOW); - - startForeground(App.STATUS_NOTIFICATION_ID, builder.build()); - } - - private native void requestVPN(); - - private native void disconnect(); - private native void connect(); -} diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt new file mode 100644 index 0000000..8683c3a --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -0,0 +1,145 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package com.tailscale.ipn + +import android.net.VpnService +import androidx.lifecycle.lifecycleScope +import android.util.Log +import android.os.Build +import android.app.PendingIntent +import android.content.Intent +import android.content.pm.PackageManager +import android.system.OsConstants +import androidx.work.WorkManager +import androidx.work.OneTimeWorkRequest +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.lifecycle.lifecycleScope +import com.tailscale.ipn.ui.service.IpnServiceManager +import kotlinx.coroutines.Job +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +class IPNService : VpnService() { + private val vpnServiceJob = Job() + private val vpnServiceScope = CoroutineScope(Dispatchers.Default + vpnServiceJob) + + private var ipnServiceManager: IpnServiceManager? = null + override fun onCreate() { + super.onCreate() + ipnServiceManager = IpnServiceManager(vpnServiceScope) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null && ACTION_STOP_VPN == intent.getAction()) { + (getApplicationContext() as App).autoConnect = false + close() + return START_NOT_STICKY + } + if (intent != null && "android.net.VpnService" == intent.getAction()) { + // Start VPN and connect to it due to Always-on VPN + val i = Intent(IPNReceiver.INTENT_CONNECT_VPN) + i.setPackage(getPackageName()) + i.setClass(getApplicationContext(), IPNReceiver::class.java) + sendBroadcast(i) + requestVPN() + ipnServiceManager?.connect() + return START_STICKY + } + requestVPN() + val app = applicationContext as App + if (app.vpnReady && app.autoConnect) { + ipnServiceManager?.connect() + } + return START_STICKY + } + + private fun close() { + stopForeground(true) + disconnect() + } + + override fun onDestroy() { + close() + vpnServiceJob.cancel() + super.onDestroy() + } + + override fun onRevoke() { + close() + super.onRevoke() + } + + private fun configIntent(): PendingIntent { + return PendingIntent.getActivity(this, 0, Intent(this, IPNActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + private fun disallowApp(b: Builder, name: String) { + try { + b.addDisallowedApplication(name) + } catch (e: Exception) { + return + } + } + + protected fun newBuilder(): Builder { + val b: Builder = Builder() + .setConfigureIntent(configIntent()) + .allowFamily(OsConstants.AF_INET) + .allowFamily(OsConstants.AF_INET6) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) b.setMetered(false) // Inherit the metered status from the underlying networks. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) b.setUnderlyingNetworks(null) // Use all available networks. + + // RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 + disallowApp(b, "com.google.android.apps.messaging") + + // Stadia https://github.com/tailscale/tailscale/issues/3460 + disallowApp(b, "com.google.stadia.android") + + // Android Auto https://github.com/tailscale/tailscale/issues/3828 + disallowApp(b, "com.google.android.projection.gearhead") + + // GoPro https://github.com/tailscale/tailscale/issues/2554 + disallowApp(b, "com.gopro.smarty") + + // Sonos https://github.com/tailscale/tailscale/issues/2548 + disallowApp(b, "com.sonos.acr") + disallowApp(b, "com.sonos.acr2") + + // Google Chromecast https://github.com/tailscale/tailscale/issues/3636 + disallowApp(b, "com.google.android.apps.chromecast.app") + return b + } + + fun notify(title: String?, message: String?) { + val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(message) + .setContentIntent(configIntent()) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + val nm: NotificationManagerCompat = NotificationManagerCompat.from(this) + nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build()) + } + + fun updateStatusNotification(title: String?, message: String?) { + val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(message) + .setContentIntent(configIntent()) + .setPriority(NotificationCompat.PRIORITY_LOW) + startForeground(App.STATUS_NOTIFICATION_ID, builder.build()) + } + + private external fun requestVPN() + private external fun disconnect() + + companion object { + const val ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN" + const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN" + } +} diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index f7094c4..c7450a9 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -17,7 +17,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.tailscale.ipn.mdm.MDMSettings -import com.tailscale.ipn.ui.service.IpnManager +import com.tailscale.ipn.ui.service.IpnViewManager import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.view.AboutView import com.tailscale.ipn.ui.view.BugReportView @@ -38,11 +38,24 @@ import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { - private val manager = IpnManager(lifecycleScope) + private val manager = IpnViewManager(lifecycleScope) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val activity = this + val viewModel = MainViewModel(manager.model, manager) + lifecycleScope.launchWhenStarted { + viewModel.tileReady.collect { isTileReady -> + App.getApplication().setTileReady(isTileReady) + } + } + lifecycleScope.launchWhenStarted{ + viewModel.readyToPrepareVPN.collect { + isReady -> if (isReady) App.getApplication().prepareVPN(activity, -1) + } + } + setContent { AppTheme { val navController = rememberNavController() @@ -63,7 +76,7 @@ class MainActivity : ComponentActivity() { composable("main") { MainView( - viewModel = MainViewModel(manager.model, manager), + viewModel = viewModel, navigation = mainViewNav ) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/service/IpnServiceManager.kt b/android/src/main/java/com/tailscale/ipn/ui/service/IpnServiceManager.kt new file mode 100644 index 0000000..e10df29 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/service/IpnServiceManager.kt @@ -0,0 +1,41 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.service + +import android.content.Intent +import android.util.Log +import com.tailscale.ipn.App +import com.tailscale.ipn.IPNReceiver +import com.tailscale.ipn.mdm.MDMSettings +import com.tailscale.ipn.ui.localapi.LocalApiClient +import com.tailscale.ipn.ui.model.Ipn +import com.tailscale.ipn.ui.notifier.Notifier +import kotlinx.coroutines.CoroutineScope + +// Actions that can be taken by the backend +interface IpnServiceActions { + fun connect() +} + +class IpnServiceManager(scope: CoroutineScope) : IpnServiceActions { + private var notifier = Notifier() + + var apiClient = LocalApiClient(scope) + var mdmSettings = MDMSettings() + val model = IpnModel(notifier, apiClient, scope) + + override fun connect() { + val context = App.getApplication().applicationContext + val callback: (com.tailscale.ipn.ui.localapi.Result) -> Unit = { result -> + if (result.successful) { + val prefs = result.success + Log.d("IpnManager","Connect: preferences updated successfully: $prefs") + } else if (result.failed) { + val error = result.error + Log.d("IpnManager","Connect: failed to update preferences: ${error?.message}") + } + } + model.setWantRunning(true, callback) + } +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/service/IpnManager.kt b/android/src/main/java/com/tailscale/ipn/ui/service/IpnViewManager.kt similarity index 74% rename from android/src/main/java/com/tailscale/ipn/ui/service/IpnManager.kt rename to android/src/main/java/com/tailscale/ipn/ui/service/IpnViewManager.kt index 4d1e47c..8d1b376 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/service/IpnManager.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/service/IpnViewManager.kt @@ -17,16 +17,15 @@ typealias PrefChangeCallback = (Result) -> Unit // Abstracts the actions that can be taken by the UI so that the concept of an IPNManager // itself is hidden from the viewModel implementations. -interface IpnActions { +interface IpnViewActions { fun startVPN() fun stopVPN() - fun connect() fun login() fun logout() fun updatePrefs(prefs: Ipn.MaskedPrefs, callback: PrefChangeCallback) } -class IpnManager(scope: CoroutineScope) : IpnActions { +class IpnViewManager(scope: CoroutineScope) : IpnViewActions { private var notifier = Notifier() var apiClient = LocalApiClient(scope) @@ -46,21 +45,7 @@ class IpnManager(scope: CoroutineScope) : IpnActions { intent.action = IPNReceiver.INTENT_DISCONNECT_VPN context.sendBroadcast(intent) } - - override fun connect() { - val context = App.getApplication().applicationContext - val callback: (com.tailscale.ipn.ui.localapi.Result) -> Unit = { result -> - if (result.successful) { - val prefs = result.success - Log.d("Connect: preferences updated successfully: $prefs") - } else if (result.failed) { - val error = result.error - Log.d("Connect: failed to update preferences: ${error?.message}") - } - } - model.setWantRunning(true, callback) - } - + override fun login() { apiClient.startLoginInteractive() } 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 c9ddeac..19c7724 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 @@ -7,9 +7,10 @@ package com.tailscale.ipn.ui.viewModel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tailscale.ipn.App import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.Ipn.State -import com.tailscale.ipn.ui.service.IpnActions +import com.tailscale.ipn.ui.service.IpnViewActions import com.tailscale.ipn.ui.service.IpnModel import com.tailscale.ipn.ui.service.set import com.tailscale.ipn.ui.util.PeerCategorizer @@ -18,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -class MainViewModel(val model: IpnModel, val actions: IpnActions) : ViewModel() { +class MainViewModel(val model: IpnModel, val actions: IpnViewActions) : ViewModel() { // The user readable state of the system val stateRes: StateFlow = MutableStateFlow(State.NoState.userStringRes()) @@ -38,12 +39,22 @@ class MainViewModel(val model: IpnModel, val actions: IpnActions) : ViewModel() // The active search term for filtering peers val searchTerm: StateFlow = MutableStateFlow("") + // The state of the Quick Settings Tile + val tileReady: StateFlow = MutableStateFlow(false) + + // Whether the VPN is ready to be prepared + val readyToPrepareVPN: StateFlow = MutableStateFlow(false) + init { viewModelScope.launch { + var previousState: State? = null model.state.collect { state -> stateRes.set(state.userStringRes()) vpnToggleState.set((state == State.Running || state == State.Starting)) + readyToPrepareVPN.set(previousState != null && previousState!! <= State.Stopped && state > State.Stopped) + tileReady.set(state >= State.Stopped) + previousState = state } } @@ -67,6 +78,8 @@ class MainViewModel(val model: IpnModel, val actions: IpnActions) : ViewModel() } fun toggleVpn() { + val stateValue = model.state.value + Log.d("MyApp", "Current VPN State: $stateValue") when (model.state.value) { State.Running -> actions.stopVPN() else -> actions.startVPN() diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index bb08a7a..fcb6a92 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -6,7 +6,7 @@ package com.tailscale.ipn.ui.viewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tailscale.ipn.R -import com.tailscale.ipn.ui.service.IpnActions +import com.tailscale.ipn.ui.service.IpnViewActions import com.tailscale.ipn.ui.service.IpnModel import com.tailscale.ipn.ui.service.toggleCorpDNS import com.tailscale.ipn.ui.view.SettingsNav @@ -38,7 +38,7 @@ data class Setting( data class SettingBundle(val title: String? = null, val settings: List) -class SettingsViewModel(val model: IpnModel, val ipnActions: IpnActions, val navigation: SettingsNav) : ViewModel() { +class SettingsViewModel(val model: IpnModel, val ipnActions: IpnViewActions, val navigation: SettingsNav) : ViewModel() { // The logged in user val user = model.loggedInUser.value diff --git a/pkg/localapiservice/localapiservice.go b/pkg/localapiservice/localapiservice.go index 6fa88f5..e567042 100644 --- a/pkg/localapiservice/localapiservice.go +++ b/pkg/localapiservice/localapiservice.go @@ -4,7 +4,9 @@ package localapiservice import ( + "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -12,6 +14,7 @@ import ( "net/http" "time" + "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" ) @@ -111,3 +114,22 @@ func (s *LocalAPIService) Logout(ctx context.Context, backend *ipnlocal.LocalBac return err } + +func (s *LocalAPIService) Start(ctx context.Context, backend *ipnlocal.LocalBackend, options ipn.Options) error { + + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + body, err := json.Marshal(options) + if err != nil { + log.Printf("start: %s", err) + return err + } + r, err := s.Call(ctx, "POST", "/localapi/v0/start", bytes.NewReader(body)) + defer r.Body().Close() + + if err != nil { + log.Printf("start: %s", err) + backend.Start(options) + } + return err +} diff --git a/pkg/tailscale/callbacks.go b/pkg/tailscale/callbacks.go index 304a4b7..7f04849 100644 --- a/pkg/tailscale/callbacks.go +++ b/pkg/tailscale/callbacks.go @@ -63,22 +63,6 @@ func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) onVPNRequested <- jnipkg.NewGlobalRef(jenv, jnipkg.Object(this)) } -//export Java_com_tailscale_ipn_IPNService_connect -func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { - onConnect <- ConnectEvent{Enable: true} -} - -//export Java_com_tailscale_ipn_IPNService_disconnect -func Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) { - jenv := (*jnipkg.Env)(unsafe.Pointer(env)) - onDisconnect <- jnipkg.NewGlobalRef(jenv, jnipkg.Object(this)) -} - -//export Java_com_tailscale_ipn_StartVPNWorker_connect -func Java_com_tailscale_ipn_StartVPNWorker_connect(env *C.JNIEnv, this C.jobject) { - onConnect <- ConnectEvent{Enable: true} -} - //export Java_com_tailscale_ipn_StopVPNWorker_disconnect func Java_com_tailscale_ipn_StopVPNWorker_disconnect(env *C.JNIEnv, this C.jobject) { onConnect <- ConnectEvent{Enable: false} diff --git a/pkg/tailscale/tailscale.go b/pkg/tailscale/tailscale.go index ee8005b..a0f208d 100644 --- a/pkg/tailscale/tailscale.go +++ b/pkg/tailscale/tailscale.go @@ -82,8 +82,6 @@ type App struct { localAPI *localapiservice.LocalAPIService backend *ipnlocal.LocalBackend - // netStates receives the most recent network state. - netStates chan BackendState // invalidates receives whenever the window should be refreshed. invalidates chan struct{} } @@ -119,21 +117,20 @@ type settingsFunc func(*router.Config, *dns.OSConfig) error const defaultMTU = 1280 // minimalMTU from wgengine/userspace.go +type ConnectEvent struct { + Enable bool +} + const ( logPrefKey = "privatelogid" loginMethodPrefKey = "loginmethod" customLoginServerPrefKey = "customloginserver" ) -type ConnectEvent struct { - Enable bool -} - func main() { a := &App{ jvm: (*jnipkg.JVM)(unsafe.Pointer(javaVM())), appCtx: jnipkg.Object(appContext()), - netStates: make(chan BackendState, 1), invalidates: make(chan struct{}, 1), } @@ -207,6 +204,8 @@ func (a *App) runBackend(ctx context.Context) error { state BackendState service jnipkg.Object // of IPNService ) + + a.localAPI.Start(ctx, b.backend, ipn.Options{}) for { select { case c := <-configs: