android: add notifier support a data model and compose dependencies

fixes ENG-2084
fixes ENG-2086

Adds support for the ipnBusWatcher directly via a JNI API rather than HTTP via LocalAPIClient

Adds a rudimentary controller class and a model from which we can construct ViewModels

Cleans up some of the JNI bindings.  Adds hooks for ensuring the JNI setup is complete before attempting to do LocalAPIClient things.

Cleans up some wildcard imports.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
pull/195/head
Jonathan Nobels 8 months ago
parent a0f87846fd
commit 0390b5016a

@ -1,6 +1,7 @@
buildscript {
ext.kotlin_version = "1.9.22"
ext.kotlin_compose_version = "1.5.10"
repositories {
google()
@ -38,6 +39,12 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "$kotlin_compose_version"
}
flavorDimensions "version"
productFlavors {
fdroid {
@ -52,15 +59,33 @@ android {
}
dependencies {
// Android dependencies.
implementation "androidx.core:core:1.9.0"
implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.browser:browser:1.5.0"
implementation "androidx.security:security-crypto:1.1.0-alpha06"
implementation "androidx.work:work-runtime:2.8.1"
// Kotlin dependencies.
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Compose dependencies.
def composeBom = platform('androidx.compose:compose-bom:2023.06.01')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3:1.0.0'
implementation "androidx.compose.ui:ui:1.4.3"
implementation "androidx.compose.ui:ui-tooling:1.4.3"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.7.2'
// Tailscale dependencies.
implementation ':ipn@aar'
// Tests
testImplementation "junit:junit:4.12"
// Non-free dependencies.

@ -70,8 +70,6 @@ import androidx.browser.customtabs.CustomTabsIntent;
import org.gioui.Gio;
import com.tailscale.ipn.ui.localapi.LocalApiClient;
public class App extends Application {
private static final String PEER_TAG = "peer";
@ -90,8 +88,6 @@ public class App extends Application {
public DnsConfig dns = new DnsConfig();
public DnsConfig getDnsConfigObj() { return this.dns; }
static final LocalApiClient api = new LocalApiClient();
@Override public void onCreate() {
super.onCreate();
// Load and initialize the Go library.

@ -6,13 +6,16 @@ package com.tailscale.ipn.ui.localapi
import android.util.Log
import com.tailscale.ipn.ui.model.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
// A response from the echo endpoint.
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
class LocalApiClient {
@ -20,6 +23,18 @@ class LocalApiClient {
Log.d("LocalApiClient", "LocalApiClient created")
}
companion object {
private val _isReady = MutableStateFlow(false)
val isReady: StateFlow<Boolean> = _isReady
// Called by the backend when the localAPI is ready to accept requests.
@JvmStatic
fun onReady() {
_isReady.value = true
Log.d("LocalApiClient", "LocalApiClient is ready")
}
}
// Perform a request to the local API in the go backend. This is
// the primary JNI method for servicing a localAPI call. This
// is GUARANTEED to call back into onResponse with the response
@ -33,26 +48,31 @@ class LocalApiClient {
// the corresponding request. Cookies must be unique for each request.
external fun doRequest(request: String, method: String, body: String, cookie: String)
protected val scope = CoroutineScope(Dispatchers.IO + Job())
fun <T> executeRequest(request: LocalAPIRequest<T>) {
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
addRequest(request)
// The jni handler will treat the empty string in the body as null.
val body = request.body ?: ""
doRequest(request.path, request.method, body, request.cookie)
scope.launch {
isReady.first { it == true }
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
addRequest(request)
// The jni handler will treat the empty string in the body as null.
val body = request.body ?: ""
doRequest(request.path, request.method, body, request.cookie)
}
}
// This is called from the JNI layer to publish localAPIResponses. This should execute on the
// same thread that called doRequest.
fun onResponse(response: String, cookie: String) {
val request = requests[cookie]
if (request != null) {
request?.let {
Log.d("LocalApiClient", "Reponse for request:${request.path} cookie:${request.cookie}")
// The response handler will invoked internally by the request parser
// The response handler will invoked internally by the request parser
request.parser(response)
removeRequest(cookie)
} else {
Log.e("LocalApiClient", "Received response for unknown request: ${cookie}")
}
?: { Log.e("LocalApiClient", "Received response for unknown request: ${cookie}") }
}
// Tracks in-flight requests and their callback handlers by cookie. This should
@ -85,6 +105,16 @@ class LocalApiClient {
executeRequest<Ipn.Prefs>(req)
}
fun getProfiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit) {
val req = LocalAPIRequest.profiles(responseHandler)
executeRequest<List<IpnLocal.LoginProfile>>(req)
}
fun getCurrentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit) {
val req = LocalAPIRequest.currentProfile(responseHandler)
executeRequest<IpnLocal.LoginProfile>(req)
}
// (jonathan) TODO: A (likely) exhaustive list of localapi endpoints required for
// a fully functioning client. This is a work in progress and will be updated
// See: corp/xcode/Shared/LocalAPIClient.swift for the various verbs, parameters,
@ -112,39 +142,4 @@ class LocalApiClient {
// ping
// setTailFSFileServerAddress
// Run some tests to validate the APIs work before we have anything
// that calls them. This runs after a short delay to avoid not-ready
// errors
// (jonathan) TODO: Do we need some kind of "onReady" callback?
// (jonathan) TODO: Remove these we're further along
fun runAPITests() = runBlocking {
delay(5000L)
getStatus { result ->
if (result.failed) {
Log.e("LocalApiClient", "Error getting status: ${result.error}")
} else {
val status = result.success
Log.d("LocalApiClient", "Got status: ${status}")
}
}
getBugReportId { result ->
if (result.failed) {
Log.e("LocalApiClient", "Error getting bug report id: ${result.error}")
} else {
val bugReportId = result.success
Log.d("LocalApiClient", "Got bug report id: ${bugReportId}")
}
}
getPrefs { result ->
if (result.failed) {
Log.e("LocalApiClient", "Error getting prefs: ${result.error}")
} else {
val prefs = result.success
Log.d("LocalApiClient", "Got prefs: ${prefs}")
}
}
}
}

@ -19,7 +19,7 @@ enum class LocalAPIEndpoint(val rawValue: String) {
LoginInteractive("login-interactive"),
ResetAuth("reset-auth"),
Logout("logout"),
Profiles("profiles"),
Profiles("profiles/"),
ProfilesCurrent("profiles/current"),
Status("status"),
TKAStatus("tka/status"),
@ -96,6 +96,20 @@ class LocalAPIRequest<T>(
}
}
fun profiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit): LocalAPIRequest<List<IpnLocal.LoginProfile>> {
val path = LocalAPIEndpoint.Profiles.path()
return LocalAPIRequest<List<IpnLocal.LoginProfile>>(path, "GET", null, responseHandler) { resp ->
responseHandler(decode<List<IpnLocal.LoginProfile>>(resp))
}
}
fun currentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit): LocalAPIRequest<IpnLocal.LoginProfile> {
val path = LocalAPIEndpoint.ProfilesCurrent.path()
return LocalAPIRequest<IpnLocal.LoginProfile>(path, "GET", null, responseHandler) { resp ->
responseHandler(decode<IpnLocal.LoginProfile>(resp))
}
}
// Check if the response was a generic error
fun parseError(respData: String): Error {
try {

@ -10,7 +10,7 @@ class Result<T> {
val success: T?
val error: Error?
constructor(success: T?, error: Error?) {
private constructor(success: T?, error: Error?) {
if (success != null && error != null) {
throw IllegalArgumentException("Result cannot have both a success and an error")
}

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
class Dns {
@Serializable data class HostEntry(val addr: Addr?, val hosts: List<String>?)

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
class Ipn {
@ -16,20 +16,14 @@ class Ipn {
NeedsMachineAuth(3),
Stopped(4),
Starting(5),
Running(6),
}
Running(6);
// NotifyWatchOpt is a bitmask of options supplied to the notifier to specify which
// what we want to see on the Noitfy bus
enum class NotifyWatchOpt(val value: Int) {
engineUpdates(0),
initialState(1 shl 1),
prefs(1 shl 2),
netmap(1 shl 3),
noPrivateKeys(1 shl 4),
initialTailFSShares(1 shl 5)
companion object {
fun fromInt(value: Int): State? {
return State.values().first { s -> s.value == value }
}
}
}
// A nofitication message recieved on the Notify bus. Fields will be populated based
// on which NotifyWatchOpts were set when the Notifier was created.
@Serializable
@ -38,7 +32,7 @@ class Ipn {
val ErrMessage: String? = null,
val LoginFinished: Empty.Message? = null,
val FilesWaiting: Empty.Message? = null,
val State: State? = null,
val State: Int? = null,
var Prefs: Prefs? = null,
var NetMap: Netmap.NetworkMap? = null,
var Engine: EngineStatus? = null,

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
class IpnState {
@Serializable

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
class Netmap {
@Serializable

@ -4,7 +4,7 @@
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
class Tailcfg {
@Serializable

@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package com.tailscale.ipn.ui.model
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
typealias Addr = String
typealias Prefix = String

@ -0,0 +1,154 @@
// Copyright (c) 2024 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.
package com.tailscale.ipn.ui.notifier
import android.util.Log
import com.tailscale.ipn.ui.model.Ipn.Notify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
typealias NotifierCallback = (Notify) -> Unit
class Watcher(
val sessionId: String,
val mask: Int,
val callback: NotifierCallback
)
// Notifier is a wrapper around the IPN Bus notifier. It provides a way to watch the
// for changes in various parts of the Tailscale engine. You will typically only use
// a single Notifier per instance of your application which lasts for the lifetime of
// the process.
//
// The primary entry point here is watchIPNBus which will start a watcher on the IPN bus
// and return you the session Id. When you are done with your watcher, you must call
// unwatchIPNBus with the sessionId.
class Notifier {
constructor() {
Log.d("Notifier", "Notifier created")
}
protected val scope = CoroutineScope(Dispatchers.IO + Job())
// NotifyWatchOpt is a bitmask of options supplied to the notifier to specify which
// what we want to see on the Noitfy bus
enum class NotifyWatchOpt(val value: Int) {
engineUpdates(1 shl 0),
initialState(1 shl 1),
prefs(1 shl 2),
netmap(1 shl 3),
noPrivateKeys(1 shl 4),
initialTailFSShares(1 shl 5)
}
companion object {
private val sessionIdLock = Any()
private var sessionId: Int = 0
private val decoder = Json { ignoreUnknownKeys = true }
private val _isReady = MutableStateFlow(false)
val isReady: StateFlow<Boolean> = _isReady
// Called by the backend when the localAPI is ready to accept requests.
@JvmStatic
fun onReady() {
_isReady.value = true
Log.d("Notifier", "Notifier is ready")
}
private fun generateSessionId(): String {
synchronized(sessionIdLock) {
sessionId += 1
return sessionId.toString()
}
}
}
// Starts an IPN Bus watcher. **This is blocking** and will not return until
// the watcher is stopped and must be excuted in a suitable coroutine scope such
// as Dispatchers.IO
private external fun startIPNBusWatcher(sessionId: String, mask: Int)
// Stops an IPN Bus watcher
private external fun stopIPNBusWatcher(sessionId: String)
private var watchers = HashMap<String, Watcher>()
// Callback from jni when a new notification is received
fun onNotify(notification: String, sessionId: String) {
val notify = decoder.decodeFromString<Notify>(notification)
val watcher = watchers[sessionId]
watcher?.let { watcher.callback(notify) }
?: { Log.e("Notifier", "Received notification for unknown session: ${sessionId}") }
}
// Watch the IPN bus for notifications
// Notifications will be passed to the caller via the callback until
// the caller calls unwatchIPNBus with the sessionId returned from this call.
fun watchIPNBus(mask: Int, callback: NotifierCallback): String {
val sessionId = generateSessionId()
val watcher = Watcher(sessionId, mask, callback)
watchers[sessionId] = watcher
scope.launch {
// Wait for the notifier to be ready
isReady.first { it == true }
Log.d("Notifier", "Starting IPN Bus watcher for sessionid: ${sessionId}")
startIPNBusWatcher(sessionId, mask)
watchers.remove(sessionId)
Log.d("Notifier", "IPN Bus watcher for sessionid:${sessionId} has halted")
}
return sessionId
}
// Cancels the watcher with the given sessionId. No errors are thrown or
// indicated for invalid sessionIds.
fun unwatchIPNBus(sessionId: String) {
stopIPNBusWatcher(sessionId)
}
// Cancels all watchers
fun cancelAllWatchers() {
for (sessionId in watchers.values.map({ it.sessionId })) {
unwatchIPNBus(sessionId)
}
}
// Returns a list of all active watchers
fun watchers(): List<Watcher> {
return watchers.values.toList()
}
// Convenience methods for watching specific parts of the IPN bus
fun watchNetMap(callback: NotifierCallback): String {
return watchIPNBus(NotifyWatchOpt.netmap.value, callback)
}
fun watchPrefs(callback: NotifierCallback): String {
return watchIPNBus(NotifyWatchOpt.prefs.value, callback)
}
fun watchEngineUpdates(callback: NotifierCallback): String {
return watchIPNBus(NotifyWatchOpt.engineUpdates.value, callback)
}
fun watchAll(callback: NotifierCallback): String {
return watchIPNBus(
NotifyWatchOpt.netmap.value or
NotifyWatchOpt.prefs.value or
NotifyWatchOpt.engineUpdates.value or
NotifyWatchOpt.initialState.value,
callback
)
}
}

@ -0,0 +1,30 @@
// Copyright (c) 2024 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.
package com.tailscale.ipn.ui.service
import android.util.Log
import com.tailscale.ipn.ui.localapi.LocalApiClient
import com.tailscale.ipn.ui.notifier.Notifier
class IpnManager {
var notifier = Notifier()
var apiClient = LocalApiClient()
val model: IpnModel
constructor() {
model = IpnModel(notifier, apiClient)
}
// We share a single instance of the IPNManager across the entire application.
companion object {
@Volatile
private var instance: IpnManager? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: IpnManager().also { instance = it }
}
}
}

@ -0,0 +1,100 @@
// Copyright (c) 2024 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.
package com.tailscale.ipn.ui.service
import android.util.Log
import com.tailscale.ipn.ui.localapi.LocalApiClient
import com.tailscale.ipn.ui.model.*
import com.tailscale.ipn.ui.notifier.Notifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
class IpnModel {
protected val scope = CoroutineScope(Dispatchers.Default + Job())
var notifierSessions: MutableList<String> = mutableListOf()
val apiClient: LocalApiClient
constructor(notifier: Notifier, apiClient: LocalApiClient) {
Log.d("IpnModel", "IpnModel created")
this.apiClient = apiClient
val session = notifier.watchAll { n -> onNotifyChange(n) }
notifierSessions.add(session)
scope.launch { loadUserProfiles() }
}
private val _state: MutableStateFlow<Ipn.State?> = MutableStateFlow(null)
private val _netmap: MutableStateFlow<Netmap.NetworkMap?> = MutableStateFlow(null)
private val _prefs: MutableStateFlow<Ipn.Prefs?> = MutableStateFlow(null)
private val _engineStatus: MutableStateFlow<Ipn.EngineStatus?> = MutableStateFlow(null)
private val _tailFSShares: MutableStateFlow<Map<String, String>?> = MutableStateFlow(null)
private val _browseToURL: MutableStateFlow<String?> = MutableStateFlow(null)
private val _loginFinished: MutableStateFlow<String?> = MutableStateFlow(null)
private val _version: MutableStateFlow<String?> = MutableStateFlow(null)
private val _loggedInUser: MutableStateFlow<IpnLocal.LoginProfile?> = MutableStateFlow(null)
private val _loginProfiles: MutableStateFlow<List<IpnLocal.LoginProfile>?> =
MutableStateFlow(null)
val state: StateFlow<Ipn.State?> = _state
val netmap: StateFlow<Netmap.NetworkMap?> = _netmap
val prefs: StateFlow<Ipn.Prefs?> = _prefs
val engineStatus: StateFlow<Ipn.EngineStatus?> = _engineStatus
val tailFSShares: StateFlow<Map<String, String>?> = _tailFSShares
val browseToURL: StateFlow<String?> = _browseToURL
val loginFinished: StateFlow<String?> = _loginFinished
val version: StateFlow<String?> = _version
val loggedInUser: StateFlow<IpnLocal.LoginProfile?> = _loggedInUser
val loginProfiles: StateFlow<List<IpnLocal.LoginProfile>?> = _loginProfiles
var isUsingExitNode: Boolean = false
get() {
return prefs.value != null
}
// Backend Observation
private suspend fun loadUserProfiles() {
LocalApiClient.isReady.first { it == true }
apiClient.getProfiles { result ->
result.success?.let { users -> _loginProfiles.value = users }
?: run { Log.e("IpnManager", "Error loading profiles: ${result.error}") }
}
apiClient.getCurrentProfile { result ->
result.success?.let { user -> _loggedInUser.value = user }
?: run { Log.e("IpnManager", "Error loading profiles: ${result.error}") }
}
}
private fun onNotifyChange(notify: Ipn.Notify) {
notify.State?.let { state -> _state.value = Ipn.State.fromInt(state) }
notify.NetMap?.let { netmap -> _netmap.value = netmap }
notify.Prefs?.let { prefs -> _prefs.value = prefs }
notify.Engine?.let { engine -> _engineStatus.value = engine }
notify.TailFSShares?.let { shares -> _tailFSShares.value = shares }
notify.BrowseToURL?.let { url -> _browseToURL.value = url }
notify.LoginFinished?.let { message -> _loginFinished.value = message.property }
notify.Version?.let { version -> _version.value = version }
}
}

@ -6,12 +6,16 @@ package localapiservice
import (
"context"
"encoding/json"
"io"
"log"
"strings"
"time"
"unsafe"
"github.com/tailscale/tailscale-android/cmd/jni"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
)
// #include <jni.h>
@ -22,8 +26,17 @@ var shim struct {
// localApiClient is a global reference to the com.tailscale.ipn.ui.localapi.LocalApiClient class.
clientClass jni.Class
// notifierClass is a global reference to the com.tailscale.ipn.ui.notifier.Notifier class.
notifierClass jni.Class
// Typically a shared LocalAPIService instance.
service *LocalAPIService
backend *ipnlocal.LocalBackend
busWatchers map[string]func()
jvm *jni.JVM
}
//export Java_com_tailscale_ipn_ui_localapi_LocalApiClient_doRequest
@ -88,12 +101,30 @@ func doLocalAPIRequest(path string, method string, body string) string {
}
// Assign a localAPIService to our shim for handling incoming localapi requests from the Kotlin side.
func SetLocalAPIService(s *LocalAPIService) {
func ConfigureShim(jvm *jni.JVM, appCtx jni.Object, s *LocalAPIService, b *ipnlocal.LocalBackend) {
shim.busWatchers = make(map[string]func())
shim.service = s
shim.backend = b
configureLocalApiJNIHandler(jvm, appCtx)
// Let the Kotlin side know we're ready to handle requests.
jni.Do(jvm, func(env *jni.Env) error {
onReadyAPI := jni.GetStaticMethodID(env, shim.clientClass, "onReady", "()V")
jni.CallStaticVoidMethod(env, shim.clientClass, onReadyAPI)
onNotifyNot := jni.GetStaticMethodID(env, shim.notifierClass, "onReady", "()V")
jni.CallStaticVoidMethod(env, shim.notifierClass, onNotifyNot)
log.Printf("LocalAPI Shim ready")
return nil
})
}
// Loads the Kotlin-side LocalApiClient class and stores it in a global reference.
func ConfigureLocalApiJNIHandler(jvm *jni.JVM, appCtx jni.Object) error {
func configureLocalApiJNIHandler(jvm *jni.JVM, appCtx jni.Object) error {
shim.jvm = jvm
return jni.Do(jvm, func(env *jni.Env) error {
loader := jni.ClassLoaderFor(env, appCtx)
cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.ui.localapi.LocalApiClient")
@ -101,6 +132,72 @@ func ConfigureLocalApiJNIHandler(jvm *jni.JVM, appCtx jni.Object) error {
return err
}
shim.clientClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))
cl, err = jni.LoadClass(env, loader, "com.tailscale.ipn.ui.notifier.Notifier")
if err != nil {
return err
}
shim.notifierClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))
return nil
})
}
//export Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher
func Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher(
env *C.JNIEnv,
cls C.jclass,
jsessionId C.jstring) {
jenv := (*jni.Env)(unsafe.Pointer(env))
sessionIdRef := jni.NewGlobalRef(jenv, jni.Object(jsessionId))
sessionId := jni.GoString(jenv, jni.String(sessionIdRef))
defer jni.DeleteGlobalRef(jenv, sessionIdRef)
cancel := shim.busWatchers[sessionId]
if cancel != nil {
log.Printf("Deregistering app layer bus watcher with sessionid: %s", sessionId)
cancel()
delete(shim.busWatchers, sessionId)
} else {
log.Printf("Error: Could not find bus watcher with sessionid: %s", sessionId)
}
}
//export Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher
func Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher(
env *C.JNIEnv,
cls C.jclass,
jsessionId C.jstring,
jmask C.jint) {
jenv := (*jni.Env)(unsafe.Pointer(env))
sessionIdRef := jni.NewGlobalRef(jenv, jni.Object(jsessionId))
sessionId := jni.GoString(jenv, jni.String(sessionIdRef))
defer jni.DeleteGlobalRef(jenv, sessionIdRef)
log.Printf("Registering app layer bus watcher with sessionid: %s", sessionId)
ctx, cancel := context.WithCancel(context.Background())
shim.busWatchers[sessionId] = cancel
opts := ipn.NotifyWatchOpt(jmask)
shim.backend.WatchNotifications(ctx, opts, func() {
// onWatchAdded
}, func(roNotify *ipn.Notify) bool {
js, err := json.Marshal(roNotify)
if err != nil {
return true
}
jni.Do(shim.jvm, func(env *jni.Env) error {
jjson := jni.JavaString(env, string(js))
onNotify := jni.GetMethodID(env, shim.notifierClass, "onNotify", "(Ljava/lang/String;Ljava/lang/String;)V")
jni.CallVoidMethod(env, jni.Object(cls), onNotify, jni.Value(jjson), jni.Value(jsessionId))
return nil
})
return true
})
}

@ -279,11 +279,6 @@ func main() {
fatalErr(err)
}
err = localapiservice.ConfigureLocalApiJNIHandler(a.jvm, a.appCtx)
if err != nil {
fatalErr(err)
}
a.store = newStateStore(a.jvm, a.appCtx)
interfaces.RegisterInterfaceGetter(a.getInterfaces)
go func() {
@ -356,8 +351,7 @@ func (a *App) runBackend(ctx context.Context) error {
h.PermitWrite = true
a.localAPI = localapiservice.New(h)
// Share the localAPI with the JNI shim
localapiservice.SetLocalAPIService(a.localAPI)
localapiservice.ConfigureShim(a.jvm, a.appCtx, a.localAPI, b.backend)
// Contrary to the documentation for VpnService.Builder.addDnsServer,
// ChromeOS doesn't fall back to the underlying network nameservers if

Loading…
Cancel
Save