cmd/tailscale,java/com/tailscale/ipn: always register the Peer Fragment

Before this change, the Peer would be registered across Activity restarts
but not after Activity destruction (for example, when the user pressed the
back button).

Use the newer Gio ViewEvent API for tracking the Activity lifecycle and
the most recent Activity reference.

Move Java calls that need an Activity from Peer to App, leaving Peer solely
as a method for receiving onActivityResult.

Fixes tailscale/tailscale#670

Signed-off-by: Elias Naur <mail@eliasnaur.com>
pull/3/head
Elias Naur 4 years ago
parent 3089ad8347
commit 1b402aebb0

@ -15,8 +15,12 @@ import android.content.SharedPreferences;
import android.content.BroadcastReceiver;
import android.provider.Settings;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.net.VpnService;
import android.view.View;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import java.io.IOException;
import java.io.File;
@ -32,9 +36,18 @@ import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import androidx.browser.customtabs.CustomTabsIntent;
import org.gioui.Gio;
public class App extends Application {
private final static int REQUEST_SIGNIN = 1001;
private final static int REQUEST_PREPARE_VPN = 1002;
private final static String PEER_TAG = "peer";
private final static Handler mainHandler = new Handler(Looper.getMainLooper());
@Override public void onCreate() {
super.onCreate();
// Load and initialize the Go library.
@ -144,12 +157,11 @@ public class App extends Application {
return str == null || str.length() == 0;
}
// Tracklifecycle adds a Peer fragment for tracking the Activity
// attachPeer adds a Peer fragment for tracking the Activity
// lifecycle.
static void trackLifecycle(View view) {
Activity act = (Activity)view.getContext();
void attachPeer(Activity act) {
FragmentTransaction ft = act.getFragmentManager().beginTransaction();
ft.add(new Peer(), "Peer");
ft.add(new Peer(), PEER_TAG);
ft.commit();
act.getFragmentManager().executePendingTransactions();
}
@ -158,5 +170,67 @@ public class App extends Application {
return getPackageManager().hasSystemFeature("android.hardware.type.pc");
}
void googleSignIn(Activity act, String serverOAuthID) {
act.runOnUiThread(new Runnable() {
@Override public void run() {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(serverOAuthID)
.requestEmail()
.build();
GoogleSignInClient client = GoogleSignIn.getClient(act, gso);
Intent signInIntent = client.getSignInIntent();
startActivityForResult(act, signInIntent, REQUEST_SIGNIN);
}
});
}
void prepareVPN(Activity act) {
act.runOnUiThread(new Runnable() {
@Override public void run() {
Intent intent = VpnService.prepare(act);
if (intent == null) {
onVPNPrepared();
} else {
startActivityForResult(act, intent, REQUEST_PREPARE_VPN);
}
}
});
}
private void startActivityForResult(Activity act, Intent intent, int request) {
Fragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG);
f.startActivityForResult(intent, request);
}
static void onActivityResult(Activity act, int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case App.REQUEST_SIGNIN:
if (resultCode == Activity.RESULT_OK) {
GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act);
onSignin(acc.getIdToken());
} else {
onSignin(null);
}
case App.REQUEST_PREPARE_VPN:
if (resultCode == Activity.RESULT_OK) {
onVPNPrepared();
}
}
}
void showURL(Activity act, String url) {
act.runOnUiThread(new Runnable() {
@Override public void run() {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
int headerColor = 0xff496495;
builder.setToolbarColor(headerColor);
CustomTabsIntent intent = builder.build();
intent.launchUrl(act, Uri.parse(url));
}
});
}
static native void onSignin(String idToken);
static native void onVPNPrepared();
private static native void onConnectivityChanged(boolean connected);
}

@ -4,89 +4,11 @@
package com.tailscale.ipn;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.Fragment;
import android.app.DialogFragment;
import android.content.Intent;
import android.net.Uri;
import android.net.VpnService;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.browser.customtabs.CustomTabsIntent;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
public class Peer extends Fragment {
private final static int REQUEST_SIGNIN = 1001;
private final static int REQUEST_PREPARE_VPN = 1002;
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_SIGNIN:
if (resultCode == Activity.RESULT_OK) {
GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(getActivity());
onSignin(acc.getIdToken());
return;
} else {
onSignin(null);
}
case REQUEST_PREPARE_VPN:
if (resultCode == Activity.RESULT_OK) {
onVPNPrepared();
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override public void onCreate(Bundle b) {
super.onCreate(b);
setRetainInstance(true);
fragmentCreated();
}
@Override public void onDestroy() {
fragmentDestroyed();
super.onDestroy();
App.onActivityResult(getActivity(), requestCode, resultCode, data);
}
public void googleSignIn(String serverOAuthID) {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(serverOAuthID)
.requestEmail()
.build();
GoogleSignInClient client = GoogleSignIn.getClient(getActivity(), gso);
Intent signInIntent = client.getSignInIntent();
startActivityForResult(signInIntent, REQUEST_SIGNIN);
}
public void prepareVPN() {
Intent intent = VpnService.prepare(getActivity());
if (intent == null) {
onVPNPrepared();
} else {
startActivityForResult(intent, REQUEST_PREPARE_VPN);
}
}
void showURL(String url) {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
int headerColor = 0xff496495;
builder.setToolbarColor(headerColor);
CustomTabsIntent intent = builder.build();
intent.launchUrl(getActivity(), Uri.parse(url));
}
private native void fragmentCreated();
private native void fragmentDestroyed();
private native void onSignin(String idToken);
private native void onVPNPrepared();
}

@ -29,12 +29,6 @@ var (
// conditions change.
onConnectivityChange = make(chan struct{}, 1)
// onPeerCreated receives global instances of Java Peer
// instances being created.
onPeerCreated = make(chan jni.Object)
// onPeerDestroyed receives new global instances of Java Peer
// instances about to be destroyed.
onPeerDestroyed = make(chan jni.Object)
// onGoogleToken receives google ID tokens.
onGoogleToken = make(chan string)
)
@ -47,28 +41,16 @@ func init() {
connected.Store(true)
}
//export Java_com_tailscale_ipn_Peer_fragmentCreated
func Java_com_tailscale_ipn_Peer_fragmentCreated(env *C.JNIEnv, this C.jobject) {
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
onPeerCreated <- jni.NewGlobalRef(jenv, jni.Object(this))
}
//export Java_com_tailscale_ipn_Peer_fragmentDestroyed
func Java_com_tailscale_ipn_Peer_fragmentDestroyed(env *C.JNIEnv, this C.jobject) {
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
onPeerDestroyed <- jni.NewGlobalRef(jenv, jni.Object(this))
}
//export Java_com_tailscale_ipn_Peer_onVPNPrepared
func Java_com_tailscale_ipn_Peer_onVPNPrepared(env *C.JNIEnv, this C.jobject) {
//export Java_com_tailscale_ipn_App_onVPNPrepared
func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {
select {
case vpnPrepared <- struct{}{}:
default:
}
}
//export Java_com_tailscale_ipn_Peer_onSignin
func Java_com_tailscale_ipn_Peer_onSignin(env *C.JNIEnv, this C.jobject, idToken C.jstring) {
//export Java_com_tailscale_ipn_App_onSignin
func Java_com_tailscale_ipn_App_onSignin(env *C.JNIEnv, class C.jclass, idToken C.jstring) {
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
tok := jni.GoString(jenv, jni.String(idToken))
onGoogleToken <- tok

@ -481,10 +481,24 @@ func (a *App) runUI() error {
if err != nil {
return err
}
a.trackLifecycle(w)
var ops op.Ops
state := new(clientState)
var peer jni.Object
var (
// activity is the most recent Android Activity reference as reported
// by Gio ViewEvents.
activity jni.Object
)
deleteActivityRef := func() {
if activity == 0 {
return
}
jni.Do(a.jvm, func(env jni.Env) error {
jni.DeleteGlobalRef(env, activity)
return nil
})
activity = 0
}
defer deleteActivityRef()
for {
select {
case <-a.vpnClosed:
@ -513,33 +527,17 @@ func (a *App) runUI() error {
a.prefs = nil
}
a.mu.Unlock()
a.updateState(peer, state)
a.updateState(activity, state)
w.Invalidate()
if peer != 0 {
if activity != 0 {
newState := state.backend.State
// Start VPN if we just logged in.
if oldState <= ipn.Stopped && newState > ipn.Stopped {
if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil {
if err := a.prepareVPN(activity); err != nil {
fatalErr(err)
}
}
}
case peer = <-onPeerCreated:
w.Invalidate()
if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil {
return err
}
}
case p := <-onPeerDestroyed:
jni.Do(a.jvm, func(env jni.Env) error {
defer jni.DeleteGlobalRef(env, p)
if jni.IsSameObject(env, peer, p) {
jni.DeleteGlobalRef(env, peer)
peer = 0
}
return nil
})
case <-vpnPrepared:
if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil {
@ -548,6 +546,20 @@ func (a *App) runUI() error {
}
case e := <-w.Events():
switch e := e.(type) {
case app.ViewEvent:
deleteActivityRef()
view := jni.Object(e.View)
if view == 0 {
break
}
activity = a.contextForView(view)
w.Invalidate()
a.attachPeer(activity)
if state.backend.State > ipn.Stopped {
if err := a.prepareVPN(activity); err != nil {
return err
}
}
case system.DestroyEvent:
return e.Err
case *system.CommandEvent:
@ -560,32 +572,24 @@ func (a *App) runUI() error {
gtx := layout.NewContext(&ops, e)
events := ui.layout(gtx, e.Insets, state)
e.Frame(gtx.Ops)
a.processUIEvents(w, events, peer, state)
a.processUIEvents(w, events, activity, state)
}
}
}
}
// trackLifecycle registers an Android Fragment instance for lifecycle
// tracking of our Activity.
func (a *App) trackLifecycle(w *app.Window) {
go func() {
w.Do(func(view uintptr) {
err := jni.Do(a.jvm, func(env jni.Env) error {
cls := jni.GetObjectClass(env, a.appCtx)
trackLifecycle := jni.GetStaticMethodID(env, cls, "trackLifecycle", "(Landroid/view/View;)V")
return jni.CallStaticVoidMethod(env, cls, trackLifecycle, jni.Value(view))
})
if err != nil {
fatalErr(err)
}
})
}()
// attachPeer registers an Android Fragment instance for
// handling onActivityResult callbacks.
func (a *App) attachPeer(act jni.Object) {
err := a.callVoidMethod(a.appCtx, "attachPeer", "(Landroid/app/Activity;)V", jni.Value(act))
if err != nil {
fatalErr(err)
}
}
func (a *App) updateState(javaPeer jni.Object, state *clientState) {
if javaPeer != 0 && state.browseURL != "" {
a.browseToURL(javaPeer, state.browseURL)
func (a *App) updateState(act jni.Object, state *clientState) {
if act != 0 && state.browseURL != "" {
a.browseToURL(act, state.browseURL)
state.browseURL = ""
}
@ -649,20 +653,24 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) {
state.Peers = peers
}
func (a *App) prepareVPN(act jni.Object) error {
return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;)V", jni.Value(act))
}
func requestBackend(e UIEvent) {
go func() {
backendEvents <- e
}()
}
func (a *App) processUIEvents(w *app.Window, events []UIEvent, peer jni.Object, state *clientState) {
func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, state *clientState) {
for _, e := range events {
switch e := e.(type) {
case ReauthEvent:
method, _ := a.store.ReadString(loginMethodPrefKey, loginMethodWeb)
switch method {
case loginMethodGoogle:
a.googleSignIn(peer)
a.googleSignIn(act)
default:
requestBackend(WebAuthEvent{})
}
@ -678,10 +686,10 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, peer jni.Object,
w.WriteClipboard(e.Text)
case GoogleAuthEvent:
a.store.WriteString(loginMethodPrefKey, loginMethodGoogle)
a.googleSignIn(peer)
a.googleSignIn(act)
case SearchEvent:
state.query = strings.ToLower(e.Query)
a.updateState(peer, state)
a.updateState(act, state)
}
}
}
@ -695,26 +703,26 @@ func (a *App) signOut() {
}
}
func (a *App) googleSignIn(peer jni.Object) {
if peer == 0 {
func (a *App) googleSignIn(act jni.Object) {
if act == 0 {
return
}
err := jni.Do(a.jvm, func(env jni.Env) error {
sid := jni.JavaString(env, serverOAuthID)
return a.callVoidMethod(peer, "googleSignIn", "(Ljava/lang/String;)V", jni.Value(sid))
return a.callVoidMethod(a.appCtx, "googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;)V", jni.Value(act), jni.Value(sid))
})
if err != nil {
fatalErr(err)
}
}
func (a *App) browseToURL(peer jni.Object, url string) {
if peer == 0 {
func (a *App) browseToURL(act jni.Object, url string) {
if act == 0 {
return
}
err := jni.Do(a.jvm, func(env jni.Env) error {
jurl := jni.JavaString(env, url)
return a.callVoidMethod(peer, "showURL", "(Ljava/lang/String;)V", jni.Value(jurl))
return a.callVoidMethod(a.appCtx, "showURL", "(Landroid/app/Activity;Ljava/lang/String;)V", jni.Value(act), jni.Value(jurl))
})
if err != nil {
fatalErr(err)
@ -732,6 +740,27 @@ func (a *App) callVoidMethod(obj jni.Object, name, sig string, args ...jni.Value
})
}
// activityForView calls View.getContext and returns a global
// reference to the result.
func (a *App) contextForView(view jni.Object) jni.Object {
if view == 0 {
panic("invalid object")
}
var ctx jni.Object
err := jni.Do(a.jvm, func(env jni.Env) error {
cls := jni.GetObjectClass(env, view)
m := jni.GetMethodID(env, cls, "getContext", "()Landroid/content/Context;")
var err error
ctx, err = jni.CallObjectMethod(env, view, m)
ctx = jni.NewGlobalRef(env, ctx)
return err
})
if err != nil {
panic(err)
}
return ctx
}
func fatalErr(err error) {
// TODO: expose in UI.
log.Printf("fatal error: %v", err)

@ -4,7 +4,7 @@ go 1.14
require (
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b
gioui.org v0.0.0-20200726090130-3b95e2918359
gioui.org v0.0.0-20200813124137-6c30c6386ccf
gioui.org/cmd v0.0.0-20200809161702-a063febed977
github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a

@ -4,6 +4,8 @@ eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b h1:J9r7EuPdhvBTafg34EqrObA
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b/go.mod h1:CYwJpIhpzVfoHpFXGlXjSx9mXMWtHt4XXmZb6RjumRc=
gioui.org v0.0.0-20200726090130-3b95e2918359 h1:lae3H0ZryBOt3Kboxa55mpo5KsASVHSdS65EsGMf9kc=
gioui.org v0.0.0-20200726090130-3b95e2918359/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org v0.0.0-20200813124137-6c30c6386ccf h1:H2Nqf70MG2GeZKrWn5W8fuPcrn4JFp/PCO/jjE1hyfY=
gioui.org v0.0.0-20200813124137-6c30c6386ccf/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org/cmd v0.0.0-20200809161702-a063febed977 h1:ZV34932OTha87lgiSpz68YPzMujPGmL0dSHww9VHIX8=
gioui.org/cmd v0.0.0-20200809161702-a063febed977/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

Loading…
Cancel
Save