Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="ShareActivity"
Expand Down
32 changes: 32 additions & 0 deletions android/src/main/java/com/tailscale/ipn/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class MainActivity : ComponentActivity() {
companion object {
private const val TAG = "Main Activity"
private const val START_AT_ROOT = "startAtRoot"
const val ACTION_SHORTCUT_CONNECT = "com.tailscale.ipn.SHORTCUT_CONNECT"
const val ACTION_SHORTCUT_DISCONNECT = "com.tailscale.ipn.SHORTCUT_DISCONNECT"
}

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

// grab app to make sure it initializes
App.get()

// Handle shortcut intents early — if triggered via shortcut, act and finish immediately
if (handleShortcutIntent(intent)) return

appViewModel = (application as App).getAppScopedViewModel()
viewModel =
ViewModelProvider(this, MainViewModelFactory(appViewModel)).get(MainViewModel::class.java)
Expand Down Expand Up @@ -469,6 +475,7 @@ class MainActivity : ComponentActivity() {

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (handleShortcutIntent(intent)) return
if (intent.getBooleanExtra(START_AT_ROOT, false)) {
if (this::navController.isInitialized) {
val previousEntry = navController.previousBackStackEntry
Expand All @@ -489,6 +496,31 @@ class MainActivity : ComponentActivity() {
}
}

/**
* Handles shortcut intents for connect/disconnect actions.
* Returns true if the intent was a shortcut action (caller should return early).
*/
private fun handleShortcutIntent(intent: Intent): Boolean {
val action = intent.action
val shortcutAction = intent.getStringExtra("shortcut_action")

return when {
action == ACTION_SHORTCUT_CONNECT || shortcutAction == "connect" -> {
TSLog.d(TAG, "Shortcut: Connect VPN triggered")
App.get().startVPN()
finish()
true
}
action == ACTION_SHORTCUT_DISCONNECT || shortcutAction == "disconnect" -> {
TSLog.d(TAG, "Shortcut: Disconnect VPN triggered")
App.get().stopVPN()
finish()
true
}
else -> false
}
}

private fun login(urlString: String) {
// Launch coroutine to listen for state changes. When the user completes login, relaunch
// MainActivity to bring the app back to focus.
Expand Down
11 changes: 11 additions & 0 deletions android/src/main/res/drawable/ic_shortcut_connect.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<!-- Power plug / connected icon -->
<path
android:fillColor="#FFFFFF"
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"/>
</vector>
11 changes: 11 additions & 0 deletions android/src/main/res/drawable/ic_shortcut_disconnect.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<!-- Power off icon -->
<path
android:fillColor="#FFFFFF"
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"/>
</vector>
3 changes: 3 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<string name="none">None</string>
<string name="connect">Connect</string>
<string name="disconnect">Disconnect</string>
<string name="shortcut_connect_long">Connect to Tailscale</string>
<string name="shortcut_disconnect_long">Disconnect from Tailscale</string>
<string name="shortcut_disabled">Tailscale shortcut is not available</string>
<string name="unknown_user">Unknown user</string>
<string name="connected">Connected</string>
<string name="using_exit_node">Using exit node (%s)</string>
Expand Down
30 changes: 30 additions & 0 deletions android/src/main/res/xml/shortcuts.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

<shortcut
android:shortcutId="connect_vpn"
android:enabled="true"
android:icon="@drawable/ic_shortcut_connect"
android:shortcutShortLabel="@string/connect"
android:shortcutLongLabel="@string/shortcut_connect_long"
android:shortcutDisabledMessage="@string/shortcut_disabled">
<intent
android:action="com.tailscale.ipn.SHORTCUT_CONNECT"
android:targetPackage="com.tailscale.ipn"
android:targetClass="com.tailscale.ipn.MainActivity" />
</shortcut>

<shortcut
android:shortcutId="disconnect_vpn"
android:enabled="true"
android:icon="@drawable/ic_shortcut_disconnect"
android:shortcutShortLabel="@string/disconnect"
android:shortcutLongLabel="@string/shortcut_disconnect_long"
android:shortcutDisabledMessage="@string/shortcut_disabled">
<intent
android:action="com.tailscale.ipn.SHORTCUT_DISCONNECT"
android:targetPackage="com.tailscale.ipn"
android:targetClass="com.tailscale.ipn.MainActivity" />
</shortcut>

</shortcuts>