Skip to content

Commit 3751ca5

Browse files
committed
Android App Shortcuts for Samsung/Google Routines (Fix for issue #9531)
Add static App Shortcuts (Connect/Disconnect) so Samsung Routines and Google Routines can trigger VPN connect/disconnect actions natively. Changes: - Create shortcuts.xml with static shortcut declarations - Add vector drawable icons for connect/disconnect - Handle shortcut intents in MainActivity (onCreate + onNewIntent) - Add shortcut-specific string resources The shortcuts target MainActivity with custom intent actions (SHORTCUT_CONNECT/SHORTCUT_DISCONNECT). When triggered, they call startVPN()/stopVPN() directly and finish() the activity immediately. Existing Tasker/broadcast functionality via IPNReceiver is preserved. Signed-off-by: keyar <keyarzera@protonmail.com>
1 parent ea5bd4c commit 3751ca5

6 files changed

Lines changed: 90 additions & 0 deletions

File tree

android/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
<intent-filter>
5858
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
5959
</intent-filter>
60+
<meta-data
61+
android:name="android.app.shortcuts"
62+
android:resource="@xml/shortcuts" />
6063
</activity>
6164
<activity
6265
android:name="ShareActivity"

android/src/main/java/com/tailscale/ipn/MainActivity.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ class MainActivity : ComponentActivity() {
120120
companion object {
121121
private const val TAG = "Main Activity"
122122
private const val START_AT_ROOT = "startAtRoot"
123+
const val ACTION_SHORTCUT_CONNECT = "com.tailscale.ipn.SHORTCUT_CONNECT"
124+
const val ACTION_SHORTCUT_DISCONNECT = "com.tailscale.ipn.SHORTCUT_DISCONNECT"
123125
}
124126

125127
private fun Context.isLandscapeCapable(): Boolean {
@@ -138,6 +140,10 @@ class MainActivity : ComponentActivity() {
138140

139141
// grab app to make sure it initializes
140142
App.get()
143+
144+
// Handle shortcut intents early — if triggered via shortcut, act and finish immediately
145+
if (handleShortcutIntent(intent)) return
146+
141147
appViewModel = (application as App).getAppScopedViewModel()
142148
viewModel =
143149
ViewModelProvider(this, MainViewModelFactory(appViewModel)).get(MainViewModel::class.java)
@@ -469,6 +475,7 @@ class MainActivity : ComponentActivity() {
469475

470476
override fun onNewIntent(intent: Intent) {
471477
super.onNewIntent(intent)
478+
if (handleShortcutIntent(intent)) return
472479
if (intent.getBooleanExtra(START_AT_ROOT, false)) {
473480
if (this::navController.isInitialized) {
474481
val previousEntry = navController.previousBackStackEntry
@@ -489,6 +496,31 @@ class MainActivity : ComponentActivity() {
489496
}
490497
}
491498

499+
/**
500+
* Handles shortcut intents for connect/disconnect actions.
501+
* Returns true if the intent was a shortcut action (caller should return early).
502+
*/
503+
private fun handleShortcutIntent(intent: Intent): Boolean {
504+
val action = intent.action
505+
val shortcutAction = intent.getStringExtra("shortcut_action")
506+
507+
return when {
508+
action == ACTION_SHORTCUT_CONNECT || shortcutAction == "connect" -> {
509+
TSLog.d(TAG, "Shortcut: Connect VPN triggered")
510+
App.get().startVPN()
511+
finish()
512+
true
513+
}
514+
action == ACTION_SHORTCUT_DISCONNECT || shortcutAction == "disconnect" -> {
515+
TSLog.d(TAG, "Shortcut: Disconnect VPN triggered")
516+
App.get().stopVPN()
517+
finish()
518+
true
519+
}
520+
else -> false
521+
}
522+
}
523+
492524
private fun login(urlString: String) {
493525
// Launch coroutine to listen for state changes. When the user completes login, relaunch
494526
// MainActivity to bring the app back to focus.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="?attr/colorControlNormal">
7+
<!-- Power plug / connected icon -->
8+
<path
9+
android:fillColor="#FFFFFF"
10+
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
11+
</vector>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="?attr/colorControlNormal">
7+
<!-- Power off icon -->
8+
<path
9+
android:fillColor="#FFFFFF"
10+
android:pathData="M13,3h-2v10h2V3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
11+
</vector>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<string name="none">None</string>
88
<string name="connect">Connect</string>
99
<string name="disconnect">Disconnect</string>
10+
<string name="shortcut_connect_long">Connect to Tailscale</string>
11+
<string name="shortcut_disconnect_long">Disconnect from Tailscale</string>
12+
<string name="shortcut_disabled">Tailscale shortcut is not available</string>
1013
<string name="unknown_user">Unknown user</string>
1114
<string name="connected">Connected</string>
1215
<string name="using_exit_node">Using exit node (%s)</string>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<shortcut
5+
android:shortcutId="connect_vpn"
6+
android:enabled="true"
7+
android:icon="@drawable/ic_shortcut_connect"
8+
android:shortcutShortLabel="@string/connect"
9+
android:shortcutLongLabel="@string/shortcut_connect_long"
10+
android:shortcutDisabledMessage="@string/shortcut_disabled">
11+
<intent
12+
android:action="com.tailscale.ipn.SHORTCUT_CONNECT"
13+
android:targetPackage="com.tailscale.ipn"
14+
android:targetClass="com.tailscale.ipn.MainActivity" />
15+
</shortcut>
16+
17+
<shortcut
18+
android:shortcutId="disconnect_vpn"
19+
android:enabled="true"
20+
android:icon="@drawable/ic_shortcut_disconnect"
21+
android:shortcutShortLabel="@string/disconnect"
22+
android:shortcutLongLabel="@string/shortcut_disconnect_long"
23+
android:shortcutDisabledMessage="@string/shortcut_disabled">
24+
<intent
25+
android:action="com.tailscale.ipn.SHORTCUT_DISCONNECT"
26+
android:targetPackage="com.tailscale.ipn"
27+
android:targetClass="com.tailscale.ipn.MainActivity" />
28+
</shortcut>
29+
30+
</shortcuts>

0 commit comments

Comments
 (0)