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.content.BroadcastReceiver;
import android.provider.Settings; import android.provider.Settings;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Uri;
import android.net.VpnService;
import android.view.View; import android.view.View;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import java.io.IOException; import java.io.IOException;
import java.io.File; 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.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import androidx.browser.customtabs.CustomTabsIntent;
import org.gioui.Gio; import org.gioui.Gio;
public class App extends Application { 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() { @Override public void onCreate() {
super.onCreate(); super.onCreate();
// Load and initialize the Go library. // Load and initialize the Go library.
@ -144,12 +157,11 @@ public class App extends Application {
return str == null || str.length() == 0; 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. // lifecycle.
static void trackLifecycle(View view) { void attachPeer(Activity act) {
Activity act = (Activity)view.getContext();
FragmentTransaction ft = act.getFragmentManager().beginTransaction(); FragmentTransaction ft = act.getFragmentManager().beginTransaction();
ft.add(new Peer(), "Peer"); ft.add(new Peer(), PEER_TAG);
ft.commit(); ft.commit();
act.getFragmentManager().executePendingTransactions(); act.getFragmentManager().executePendingTransactions();
} }
@ -158,5 +170,67 @@ public class App extends Application {
return getPackageManager().hasSystemFeature("android.hardware.type.pc"); 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); private static native void onConnectivityChanged(boolean connected);
} }

@ -4,89 +4,11 @@
package com.tailscale.ipn; package com.tailscale.ipn;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.Fragment; import android.app.Fragment;
import android.app.DialogFragment;
import android.content.Intent; 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 { 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) { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { App.onActivityResult(getActivity(), requestCode, resultCode, data);
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();
}
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. // conditions change.
onConnectivityChange = make(chan struct{}, 1) 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 receives google ID tokens.
onGoogleToken = make(chan string) onGoogleToken = make(chan string)
) )
@ -47,28 +41,16 @@ func init() {
connected.Store(true) connected.Store(true)
} }
//export Java_com_tailscale_ipn_Peer_fragmentCreated //export Java_com_tailscale_ipn_App_onVPNPrepared
func Java_com_tailscale_ipn_Peer_fragmentCreated(env *C.JNIEnv, this C.jobject) { func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {
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) {
select { select {
case vpnPrepared <- struct{}{}: case vpnPrepared <- struct{}{}:
default: default:
} }
} }
//export Java_com_tailscale_ipn_Peer_onSignin //export Java_com_tailscale_ipn_App_onSignin
func Java_com_tailscale_ipn_Peer_onSignin(env *C.JNIEnv, this C.jobject, idToken C.jstring) { func Java_com_tailscale_ipn_App_onSignin(env *C.JNIEnv, class C.jclass, idToken C.jstring) {
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env))) jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
tok := jni.GoString(jenv, jni.String(idToken)) tok := jni.GoString(jenv, jni.String(idToken))
onGoogleToken <- tok onGoogleToken <- tok

@ -481,10 +481,24 @@ func (a *App) runUI() error {
if err != nil { if err != nil {
return err return err
} }
a.trackLifecycle(w)
var ops op.Ops var ops op.Ops
state := new(clientState) 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 { for {
select { select {
case <-a.vpnClosed: case <-a.vpnClosed:
@ -513,41 +527,39 @@ func (a *App) runUI() error {
a.prefs = nil a.prefs = nil
} }
a.mu.Unlock() a.mu.Unlock()
a.updateState(peer, state) a.updateState(activity, state)
w.Invalidate() w.Invalidate()
if peer != 0 { if activity != 0 {
newState := state.backend.State newState := state.backend.State
// Start VPN if we just logged in. // Start VPN if we just logged in.
if oldState <= ipn.Stopped && newState > ipn.Stopped { 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) fatalErr(err)
} }
} }
} }
case peer = <-onPeerCreated: case <-vpnPrepared:
w.Invalidate()
if state.backend.State > ipn.Stopped { if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil { if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil {
return err return err
} }
} }
case p := <-onPeerDestroyed: case e := <-w.Events():
jni.Do(a.jvm, func(env jni.Env) error { switch e := e.(type) {
defer jni.DeleteGlobalRef(env, p) case app.ViewEvent:
if jni.IsSameObject(env, peer, p) { deleteActivityRef()
jni.DeleteGlobalRef(env, peer) view := jni.Object(e.View)
peer = 0 if view == 0 {
break
} }
return nil activity = a.contextForView(view)
}) w.Invalidate()
case <-vpnPrepared: a.attachPeer(activity)
if state.backend.State > ipn.Stopped { if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil { if err := a.prepareVPN(activity); err != nil {
return err return err
} }
} }
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent: case system.DestroyEvent:
return e.Err return e.Err
case *system.CommandEvent: case *system.CommandEvent:
@ -560,32 +572,24 @@ func (a *App) runUI() error {
gtx := layout.NewContext(&ops, e) gtx := layout.NewContext(&ops, e)
events := ui.layout(gtx, e.Insets, state) events := ui.layout(gtx, e.Insets, state)
e.Frame(gtx.Ops) e.Frame(gtx.Ops)
a.processUIEvents(w, events, peer, state) a.processUIEvents(w, events, activity, state)
} }
} }
} }
} }
// trackLifecycle registers an Android Fragment instance for lifecycle // attachPeer registers an Android Fragment instance for
// tracking of our Activity. // handling onActivityResult callbacks.
func (a *App) trackLifecycle(w *app.Window) { func (a *App) attachPeer(act jni.Object) {
go func() { err := a.callVoidMethod(a.appCtx, "attachPeer", "(Landroid/app/Activity;)V", jni.Value(act))
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 { if err != nil {
fatalErr(err) fatalErr(err)
} }
})
}()
} }
func (a *App) updateState(javaPeer jni.Object, state *clientState) { func (a *App) updateState(act jni.Object, state *clientState) {
if javaPeer != 0 && state.browseURL != "" { if act != 0 && state.browseURL != "" {
a.browseToURL(javaPeer, state.browseURL) a.browseToURL(act, state.browseURL)
state.browseURL = "" state.browseURL = ""
} }
@ -649,20 +653,24 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) {
state.Peers = peers 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) { func requestBackend(e UIEvent) {
go func() { go func() {
backendEvents <- e 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 { for _, e := range events {
switch e := e.(type) { switch e := e.(type) {
case ReauthEvent: case ReauthEvent:
method, _ := a.store.ReadString(loginMethodPrefKey, loginMethodWeb) method, _ := a.store.ReadString(loginMethodPrefKey, loginMethodWeb)
switch method { switch method {
case loginMethodGoogle: case loginMethodGoogle:
a.googleSignIn(peer) a.googleSignIn(act)
default: default:
requestBackend(WebAuthEvent{}) requestBackend(WebAuthEvent{})
} }
@ -678,10 +686,10 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, peer jni.Object,
w.WriteClipboard(e.Text) w.WriteClipboard(e.Text)
case GoogleAuthEvent: case GoogleAuthEvent:
a.store.WriteString(loginMethodPrefKey, loginMethodGoogle) a.store.WriteString(loginMethodPrefKey, loginMethodGoogle)
a.googleSignIn(peer) a.googleSignIn(act)
case SearchEvent: case SearchEvent:
state.query = strings.ToLower(e.Query) 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) { func (a *App) googleSignIn(act jni.Object) {
if peer == 0 { if act == 0 {
return return
} }
err := jni.Do(a.jvm, func(env jni.Env) error { err := jni.Do(a.jvm, func(env jni.Env) error {
sid := jni.JavaString(env, serverOAuthID) 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 { if err != nil {
fatalErr(err) fatalErr(err)
} }
} }
func (a *App) browseToURL(peer jni.Object, url string) { func (a *App) browseToURL(act jni.Object, url string) {
if peer == 0 { if act == 0 {
return return
} }
err := jni.Do(a.jvm, func(env jni.Env) error { err := jni.Do(a.jvm, func(env jni.Env) error {
jurl := jni.JavaString(env, url) 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 { if err != nil {
fatalErr(err) 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) { func fatalErr(err error) {
// TODO: expose in UI. // TODO: expose in UI.
log.Printf("fatal error: %v", err) log.Printf("fatal error: %v", err)

@ -4,7 +4,7 @@ go 1.14
require ( require (
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b 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 gioui.org/cmd v0.0.0-20200809161702-a063febed977
github.com/go-bindata/go-bindata v3.1.2+incompatible github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a 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= 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 h1:lae3H0ZryBOt3Kboxa55mpo5KsASVHSdS65EsGMf9kc=
gioui.org v0.0.0-20200726090130-3b95e2918359/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= 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 h1:ZV34932OTha87lgiSpz68YPzMujPGmL0dSHww9VHIX8=
gioui.org/cmd v0.0.0-20200809161702-a063febed977/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk= gioui.org/cmd v0.0.0-20200809161702-a063febed977/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

Loading…
Cancel
Save