Skip to content

Commit 76b307f

Browse files
committed
android: add launcher shortcuts to connect and disconnect the VPN
Add an AutomationActivity trampoline and two static launcher shortcuts (Connect, Disconnect) wired to the existing CONNECT_VPN / DISCONNECT_VPN intents. The actions can also be invoked from Samsung Modes and Routines or Tasker. The activity exists because starting an activity reliably wakes an app that the OS has force-stopped or deep-slept (e.g. Samsung battery management) and lets connect start the foreground VPN service, whereas the broadcast/Worker path is unreliable from that state. Exit-node automation is unchanged and remains available via the existing IPNReceiver USE_EXIT_NODE broadcast once the VPN is connected. Updates tailscale/tailscale#10831 Updates tailscale/tailscale#14148 Updates tailscale/tailscale#13623 Updates tailscale/tailscale#18847 Updates tailscale/tailscale#9531 Updates tailscale/tailscale#9497 Updates tailscale/tailscale#16415 Updates tailscale/tailscale#17855 Updates tailscale/tailscale#17738 Signed-off-by: Brett Jenkins <brett@brettjenkins.co.uk>
1 parent d8f28f4 commit 76b307f

4 files changed

Lines changed: 121 additions & 0 deletions

File tree

android/src/main/AndroidManifest.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@
5757
<intent-filter>
5858
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
5959
</intent-filter>
60+
<!-- Static app shortcuts must be attached to the LAUNCHER activity. -->
61+
<meta-data
62+
android:name="android.app.shortcuts"
63+
android:resource="@xml/shortcuts" />
64+
</activity>
65+
66+
<!--
67+
Transparent, no-UI trampoline that connects or disconnects the VPN. Exported so the
68+
actions show up in the launcher long-press shortcut menu, Samsung Modes & Routines, and
69+
can be sent from Tasker. Starting an activity (rather than IPNReceiver's broadcast)
70+
reliably wakes a stopped app so connect can start the foreground VPN service.
71+
-->
72+
<activity
73+
android:name="AutomationActivity"
74+
android:exported="true"
75+
android:excludeFromRecents="true"
76+
android:noHistory="true"
77+
android:taskAffinity=""
78+
android:theme="@android:style/Theme.NoDisplay">
79+
<intent-filter>
80+
<action android:name="com.tailscale.ipn.CONNECT_VPN" />
81+
<action android:name="com.tailscale.ipn.DISCONNECT_VPN" />
82+
<category android:name="android.intent.category.DEFAULT" />
83+
</intent-filter>
6084
</activity>
6185
<activity
6286
android:name="ShareActivity"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
package com.tailscale.ipn
4+
5+
import android.app.Activity
6+
import android.content.Intent
7+
import android.net.VpnService
8+
import android.os.Bundle
9+
import com.tailscale.ipn.util.TSLog
10+
11+
/**
12+
* AutomationActivity is a transparent, no-UI trampoline that connects or disconnects the VPN and
13+
* finishes immediately. It backs the launcher long-press shortcuts and can also be invoked from
14+
* Samsung Modes and Routines or Tasker.
15+
*
16+
* It is an activity rather than an extension of [IPNReceiver]'s broadcasts because starting an
17+
* activity reliably wakes an app that the OS has force-stopped or put into deep sleep, e.g. under
18+
* Samsung battery management. The brief foreground also lets connect start the VPN service without
19+
* hitting the Android 12+ restriction on starting a foreground service from the background, which
20+
* the broadcast path can trip when the app has been idle.
21+
*/
22+
class AutomationActivity : Activity() {
23+
24+
override fun onCreate(savedInstanceState: Bundle?) {
25+
super.onCreate(savedInstanceState)
26+
27+
when (intent?.action) {
28+
IPNReceiver.INTENT_CONNECT_VPN -> connect()
29+
IPNReceiver.INTENT_DISCONNECT_VPN -> UninitializedApp.get().stopVPN()
30+
else -> TSLog.w(TAG, "unknown action: ${intent?.action}")
31+
}
32+
33+
// Never show any UI: finish before this activity becomes visible.
34+
finish()
35+
}
36+
37+
/**
38+
* Starts the VPN directly when it is ready and consent is already granted. Otherwise (not set up,
39+
* or consent not yet granted) opens the app, which handles login and the consent prompt.
40+
*/
41+
private fun connect() {
42+
val app = UninitializedApp.get()
43+
if (app.isAbleToStartVPN() && VpnService.prepare(this) == null) {
44+
app.startVPN()
45+
} else {
46+
launchMainActivity()
47+
}
48+
}
49+
50+
private fun launchMainActivity() {
51+
val intent =
52+
Intent(this, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
53+
try {
54+
startActivity(intent)
55+
} catch (e: Exception) {
56+
TSLog.e(TAG, "Failed to launch MainActivity: $e")
57+
}
58+
}
59+
60+
companion object {
61+
private const val TAG = "AutomationActivity"
62+
}
63+
}

android/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,4 +378,10 @@
378378
<string name="enable_hardware_attestation">Enable hardware attestation</string>
379379
<string name="use_hardware_backed_keys_to_bind_node_identity_to_the_device">Use hardware-backed keys to bind node identity to the device</string>
380380

381+
<!-- App shortcuts (launcher long-press, Tasker, Samsung Modes &amp; Routines) -->
382+
<string name="shortcut_connect_short">Connect</string>
383+
<string name="shortcut_connect_long">Connect to Tailscale</string>
384+
<string name="shortcut_disconnect_short">Disconnect</string>
385+
<string name="shortcut_disconnect_long">Disconnect from Tailscale</string>
386+
381387
</resources>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Copyright (c) Tailscale Inc & AUTHORS -->
3+
<!-- SPDX-License-Identifier: BSD-3-Clause -->
4+
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
5+
<shortcut
6+
android:shortcutId="connect"
7+
android:enabled="true"
8+
android:icon="@drawable/ic_launcher_foreground"
9+
android:shortcutShortLabel="@string/shortcut_connect_short"
10+
android:shortcutLongLabel="@string/shortcut_connect_long">
11+
<intent
12+
android:action="com.tailscale.ipn.CONNECT_VPN"
13+
android:targetPackage="com.tailscale.ipn"
14+
android:targetClass="com.tailscale.ipn.AutomationActivity" />
15+
</shortcut>
16+
17+
<shortcut
18+
android:shortcutId="disconnect"
19+
android:enabled="true"
20+
android:icon="@drawable/ic_launcher_foreground"
21+
android:shortcutShortLabel="@string/shortcut_disconnect_short"
22+
android:shortcutLongLabel="@string/shortcut_disconnect_long">
23+
<intent
24+
android:action="com.tailscale.ipn.DISCONNECT_VPN"
25+
android:targetPackage="com.tailscale.ipn"
26+
android:targetClass="com.tailscale.ipn.AutomationActivity" />
27+
</shortcut>
28+
</shortcuts>

0 commit comments

Comments
 (0)