cmd/tailscale,java: refactor Google Sign-In into separate class

In preparation for the F-Droid release, refactor the non-free Google dependency
into a separate Java class and make the Go client tolerate missing support.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
pull/4/head
Elias Naur 4 years ago
parent 412fe8ad68
commit 6d9acbb479

@ -31,19 +31,11 @@ import java.security.GeneralSecurityException;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
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;
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());
@ -103,13 +95,6 @@ public class App extends Application {
);
}
void googleSignOut() {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.build();
GoogleSignInClient client = GoogleSignIn.getClient(this, gso);
client.signOut();
}
void setTileStatus(boolean wantRunning) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return;
@ -174,54 +159,24 @@ 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) {
void prepareVPN(Activity act, int reqCode) {
act.runOnUiThread(new Runnable() {
@Override public void run() {
Intent intent = VpnService.prepare(act);
if (intent == null) {
onVPNPrepared();
} else {
startActivityForResult(act, intent, REQUEST_PREPARE_VPN);
startActivityForResult(act, intent, reqCode);
}
}
});
}
private void startActivityForResult(Activity act, Intent intent, int request) {
static 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() {
@ -234,7 +189,6 @@ public class App extends Application {
});
}
static native void onSignin(String idToken);
static native void onVPNPrepared();
private static native void onConnectivityChanged(boolean connected);
}

@ -0,0 +1,43 @@
// 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.
package com.tailscale.ipn;
import android.app.Activity;
import android.content.Intent;
import android.content.Context;
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;
// Google implements helpers for Google services.
public final class Google {
static String getIdTokenForActivity(Activity act) {
GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act);
return acc.getIdToken();
}
static void googleSignIn(Activity act, String serverOAuthID, int reqCode) {
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();
App.startActivityForResult(act, signInIntent, reqCode);
}
});
}
static void googleSignOut(Context ctx) {
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.build();
GoogleSignInClient client = GoogleSignIn.getClient(ctx, gso);
client.signOut();
}
}

@ -4,11 +4,14 @@
package com.tailscale.ipn;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
public class Peer extends Fragment {
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
App.onActivityResult(getActivity(), requestCode, resultCode, data);
onActivityResult0(getActivity(), requestCode, resultCode);
}
private static native void onActivityResult0(Activity act, int reqCode, int resCode);
}

@ -41,21 +41,30 @@ func init() {
connected.Store(true)
}
const (
// Request codes for Android callbacks.
// requestSignin is for Google Sign-In.
requestSignin C.jint = 1000 + iota
// requestPrepareVPN is for when Android's VpnService.prepare
// completes.
requestPrepareVPN
)
// resultOK is Android's Activity.RESULT_OK.
const resultOK = -1
//export Java_com_tailscale_ipn_App_onVPNPrepared
func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {
onVPNPrepared()
}
func onVPNPrepared() {
select {
case vpnPrepared <- struct{}{}:
default:
}
}
//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
}
//export Java_com_tailscale_ipn_IPNService_connect
func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
@ -81,3 +90,29 @@ func Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclas
func Java_com_tailscale_ipn_QuickToggleService_onTileClick(env *C.JNIEnv, cls C.jclass) {
requestBackend(ToggleEvent{})
}
//export Java_com_tailscale_ipn_Peer_onActivityResult0
func Java_com_tailscale_ipn_Peer_onActivityResult0(env *C.JNIEnv, cls C.jclass, act C.jobject, reqCode, resCode C.jint) {
switch reqCode {
case requestSignin:
if resCode != resultOK {
onGoogleToken <- ""
break
}
jenv := jni.EnvFor(uintptr(unsafe.Pointer(env)))
m := jni.GetStaticMethodID(jenv, googleClass,
"getIdTokenForActivity", "(Landroid/app/Activity;)Ljava/lang/String;")
idToken, err := jni.CallStaticObjectMethod(jenv, googleClass, m, jni.Value(act))
if err != nil {
fatalErr(err)
break
}
tok := jni.GoString(jenv, jni.String(idToken))
onGoogleToken <- tok
case requestPrepareVPN:
if resCode != resultOK {
break
}
onVPNPrepared()
}
}

@ -27,9 +27,11 @@ import (
//go:generate go run github.com/go-bindata/go-bindata/go-bindata -nocompress -o logo.go tailscale.png google.png
type App struct {
jvm jni.JVM
jvm jni.JVM
// appCtx is a global reference to the com.tailscale.ipn.App instance.
appCtx jni.Object
store *stateStore
store *stateStore
// updates is notifies whenever netState or browseURL changes.
updates chan struct{}
@ -52,6 +54,11 @@ type App struct {
prefs *ipn.Prefs
}
var (
// googleClass is a global reference to the com.tailscale.ipn.Google class.
googleClass jni.Class
)
type clientState struct {
browseURL string
backend BackendState
@ -111,6 +118,19 @@ func main() {
updates: make(chan struct{}, 1),
vpnClosed: make(chan struct{}, 1),
}
err := jni.Do(a.jvm, func(env jni.Env) error {
loader := jni.ClassLoaderFor(env, a.appCtx)
cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.Google")
if err != nil {
// Ignore load errors; the Google class is not included in F-Droid builds.
return nil
}
googleClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))
return nil
})
if err != nil {
fatalErr(err)
}
a.store = newStateStore(a.jvm, a.appCtx)
go func() {
if err := a.runBackend(); err != nil {
@ -369,6 +389,10 @@ func (a *App) modelName() string {
return model
}
func googleSignInEnabled() bool {
return googleClass != 0
}
// updateNotification updates the foreground persistent status notification.
func (a *App) updateNotification(service jni.Object, state ipn.State) error {
var msg, title string
@ -652,7 +676,8 @@ func (a *App) updateState(act jni.Object, state *clientState) {
}
func (a *App) prepareVPN(act jni.Object) error {
return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;)V", jni.Value(act))
return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;I)V",
jni.Value(act), jni.Value(requestPrepareVPN))
}
func requestBackend(e UIEvent) {
@ -693,8 +718,13 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s
}
func (a *App) signOut() {
if googleClass == 0 {
return
}
err := jni.Do(a.jvm, func(env jni.Env) error {
return a.callVoidMethod(a.appCtx, "googleSignOut", "()V")
m := jni.GetStaticMethodID(env, googleClass,
"googleSignOut", "(Landroid/content/Context;)V")
return jni.CallStaticVoidMethod(env, googleClass, m, jni.Value(a.appCtx))
})
if err != nil {
fatalErr(err)
@ -702,12 +732,15 @@ func (a *App) signOut() {
}
func (a *App) googleSignIn(act jni.Object) {
if act == 0 {
if act == 0 || googleClass == 0 {
return
}
err := jni.Do(a.jvm, func(env jni.Env) error {
sid := jni.JavaString(env, serverOAuthID)
return a.callVoidMethod(a.appCtx, "googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;)V", jni.Value(act), jni.Value(sid))
m := jni.GetStaticMethodID(env, googleClass,
"googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;I)V")
return jni.CallStaticVoidMethod(env, googleClass, m,
jni.Value(act), jni.Value(sid), jni.Value(requestSignin))
})
if err != nil {
fatalErr(err)

@ -33,8 +33,6 @@ import (
_ "image/png"
)
const enableGoogleSignin = true
type UI struct {
theme *material.Theme
store *stateStore
@ -339,7 +337,7 @@ func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimen
border := widget.Border{Color: rgb(textColor), CornerRadius: unit.Dp(4), Width: unit.Px(1)}
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
if !enableGoogleSignin {
if !googleSignInEnabled() {
return D{}
}
return layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D {
@ -374,7 +372,7 @@ func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimen
}),
layout.Rigid(func(gtx C) D {
label := "Sign in with other"
if !enableGoogleSignin {
if !googleSignInEnabled() {
label = "Sign in"
}
return ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D {

Loading…
Cancel
Save