diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index ab1b2a6..fec93a6 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -10,7 +10,7 @@
+
+
+
@@ -26,6 +29,18 @@
+
+
+
+
+
+
diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java
index 97e042f..41c00f1 100644
--- a/android/src/main/java/com/tailscale/ipn/App.java
+++ b/android/src/main/java/com/tailscale/ipn/App.java
@@ -83,6 +83,10 @@ public class App extends Application {
);
}
+ void setTileStatus(boolean wantRunning) {
+ QuickToggleService.setStatus(this, wantRunning);
+ }
+
String getHostname() {
String userConfiguredDeviceName = getUserConfiguredDeviceName();
if (!isEmpty(userConfiguredDeviceName)) return userConfiguredDeviceName;
diff --git a/android/src/main/java/com/tailscale/ipn/QuickToggleService.java b/android/src/main/java/com/tailscale/ipn/QuickToggleService.java
new file mode 100644
index 0000000..f866866
--- /dev/null
+++ b/android/src/main/java/com/tailscale/ipn/QuickToggleService.java
@@ -0,0 +1,31 @@
+// 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.content.Context;
+import android.content.ComponentName;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+public class QuickToggleService extends TileService {
+ private static boolean active;
+
+ @Override public void onStartListening() {
+ Tile t = getQsTile();
+ t.setState(active ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
+ t.updateTile();
+ }
+
+ @Override public void onClick() {
+ onTileClick();
+ }
+
+ static void setStatus(Context ctx, boolean wantRunning) {
+ active = wantRunning;
+ requestListeningState(ctx, new ComponentName(ctx, QuickToggleService.class));
+ }
+
+ private static native void onTileClick();
+}
diff --git a/android/src/main/res/drawable/ic_tile.xml b/android/src/main/res/drawable/ic_tile.xml
new file mode 100644
index 0000000..3cd5907
--- /dev/null
+++ b/android/src/main/res/drawable/ic_tile.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
new file mode 100644
index 0000000..07b3455
--- /dev/null
+++ b/android/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Tailscale
+ Tailscale
+
diff --git a/cmd/tailscale/callbacks.go b/cmd/tailscale/callbacks.go
index f784bbb..d021329 100644
--- a/cmd/tailscale/callbacks.go
+++ b/cmd/tailscale/callbacks.go
@@ -94,3 +94,8 @@ func Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclas
default:
}
}
+
+//export Java_com_tailscale_ipn_QuickToggleService_onTileClick
+func Java_com_tailscale_ipn_QuickToggleService_onTileClick(env *C.JNIEnv, cls C.jclass) {
+ requestBackend(ToggleEvent{})
+}
diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go
index aa795ad..8e672f6 100644
--- a/cmd/tailscale/main.go
+++ b/cmd/tailscale/main.go
@@ -40,7 +40,7 @@ type App struct {
// backend is the channel for events from the frontend to the
// backend.
- backend chan UIEvent
+ backendEvents chan UIEvent
// mu protects the following fields.
mu sync.Mutex
@@ -83,13 +83,14 @@ type SearchEvent struct {
Query string
}
-type ReauthEvent struct{}
-
-type WebAuthEvent struct{}
-
-type GoogleAuthEvent struct{}
-
-type LogoutEvent struct{}
+// UIEvent types.
+type (
+ ToggleEvent struct{}
+ ReauthEvent struct{}
+ WebAuthEvent struct{}
+ GoogleAuthEvent struct{}
+ LogoutEvent struct{}
+)
// serverOAuthID is the OAuth ID of the tailscale-android server, used
// by GoogleSignInOptions.Builder.requestIdToken.
@@ -97,13 +98,15 @@ const serverOAuthID = "744055068597-hv4opg0h7vskq1hv37nq3u26t8c15qk0.apps.google
const enabledKey = "ipn_enabled"
+// backendEvents receives events from the UI (Activity, Tile etc.) to the backend.
+var backendEvents = make(chan UIEvent)
+
func main() {
a := &App{
jvm: jni.JVMFor(app.JavaVM()),
appCtx: jni.Object(app.AppContext()),
updates: make(chan struct{}, 1),
vpnClosed: make(chan struct{}, 1),
- backend: make(chan UIEvent),
}
appDir, err := app.DataDir()
if err != nil {
@@ -232,8 +235,11 @@ func (a *App) runBackend() error {
if m := state.NetworkMap; m != nil && service != 0 {
alarm(a.notifyExpiry(service, m.Expiry))
}
- case e := <-a.backend:
+ case e := <-backendEvents:
switch e := e.(type) {
+ case ToggleEvent:
+ prefs.WantRunning = !prefs.WantRunning
+ go b.backend.SetPrefs(prefs)
case WebAuthEvent:
if !signingIn {
go b.backend.StartLoginInteractive()
@@ -388,7 +394,14 @@ func (a *App) notify(state BackendState) {
func (a *App) setPrefs(prefs *ipn.Prefs) {
a.mu.Lock()
a.prefs = prefs
+ wantRunning := jni.True
+ if !prefs.WantRunning {
+ wantRunning = jni.False
+ }
a.mu.Unlock()
+ if err := a.callVoidMethod(a.appCtx, "setTileStatus", "(Z)V", jni.Value(wantRunning)); err != nil {
+ fatalErr(err)
+ }
select {
case a.updates <- struct{}{}:
default:
@@ -418,7 +431,7 @@ func (a *App) runUI() error {
for {
select {
case <-a.vpnClosed:
- a.request(ConnectEvent{Enable: false})
+ requestBackend(ConnectEvent{Enable: false})
case <-a.updates:
a.mu.Lock()
oldState := state.backend.State
@@ -563,9 +576,9 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) {
state.Peers = peers
}
-func (a *App) request(e UIEvent) {
+func requestBackend(e UIEvent) {
go func() {
- a.backend <- e
+ backendEvents <- e
}()
}
@@ -578,15 +591,15 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, peer jni.Object,
case loginMethodGoogle:
a.googleSignIn(peer)
default:
- a.request(WebAuthEvent{})
+ requestBackend(WebAuthEvent{})
}
case WebAuthEvent:
a.store.WriteString(loginMethodPrefKey, loginMethodWeb)
- a.request(e)
+ requestBackend(e)
case LogoutEvent:
- a.request(e)
+ requestBackend(e)
case ConnectEvent:
- a.request(e)
+ requestBackend(e)
case CopyEvent:
w.WriteClipboard(e.Text)
case GoogleAuthEvent:
diff --git a/jni/jni.go b/jni/jni.go
index b3485d7..b772555 100644
--- a/jni/jni.go
+++ b/jni/jni.go
@@ -40,9 +40,15 @@ type (
MethodID C.jmethodID
String C.jstring
ByteArray C.jbyteArray
+ Boolean C.jboolean
Value uint64 // All JNI types fit into 64-bits.
)
+const (
+ True Boolean = C.JNI_TRUE
+ False Boolean = C.JNI_FALSE
+)
+
func JVMFor(jvmPtr uintptr) JVM {
return JVM{
jvm: (*C.JavaVM)(unsafe.Pointer(jvmPtr)),