Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dynamic Shortcuts integration #48

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ buildscript {
materialComponentsVersion = '1.6.1'
preferenceVersion = '1.2.0'
zxingEmbeddedVersion = '4.3.0'
shortcutsVersion = '1.0.0'

groupName = 'com.wireguard.android'
}
Expand Down
1 change: 1 addition & 0 deletions ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.core:core-google-shortcuts:$shortcutsVersion"
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
Expand Down
13 changes: 13 additions & 0 deletions ui/sampledata/interface_names.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@
{ "checked": true },
{ "checked": false },
{ "checked": true }
],
"shortcut": [
{ "shortcut": true },
{ "shortcut": false },
{ "shortcut": false },
{ "shortcut": false },
{ "shortcut": false },
{ "shortcut": false },
{ "shortcut": true },
{ "shortcut": true },
{ "shortcut": true },
{ "shortcut": false },
{ "shortcut": false }
]
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,48 @@ import com.wireguard.android.backend.Tunnel
import com.wireguard.android.util.ErrorMessages
import kotlinx.coroutines.launch

@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }

private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
lifecycleScope.launch {
val tunnelAction = when(intent.action) {
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
else -> Tunnel.State.TOGGLE // Implicit toggle to keep previous behaviour
}

val tunnel = when(val tunnelName = intent.getStringExtra("tunnel")) {
null -> Application.getTunnelManager().lastUsedTunnel
else -> Application.getTunnelManager().getTunnels().find { it.name == tunnelName }
} ?: return@launch // If we failed to identify the tunnel, just return

try {
tunnel.setStateAsync(Tunnel.State.TOGGLE)
tunnel.setStateAsync(tunnelAction)
} catch (e: Throwable) {
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
updateTileService()
val error = ErrorMessages[e]
val message = getString(R.string.toggle_error, error)
Log.e(TAG, message, e)
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
finishAffinity()
return@launch
}
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
updateTileService()
finishAffinity()
}
}

/**
* TileService is only available for API 24+, if it's available it'll be updated,
* otherwise it's ignored.
*/
private fun updateTileService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
Expand Down
13 changes: 13 additions & 0 deletions ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package com.wireguard.android.fragment
import android.content.Context
import android.util.Log
import android.view.View
import android.widget.CompoundButton
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.databinding.DataBindingUtil
Expand Down Expand Up @@ -100,6 +101,18 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
}

fun setShortcutState(view: CompoundButton, checked: Boolean) {
val tunnel = when (val binding = DataBindingUtil.findBinding<ViewDataBinding>(view)) {
is TunnelDetailFragmentBinding -> binding.tunnel
is TunnelListItemBinding -> binding.item
else -> return
} ?: return
val activity = activity ?: return
activity.lifecycleScope.launch {
tunnel.setShortcutsAsync(checked)
}
}

companion object {
private const val TAG = "WireGuard/BaseFragment"
}
Expand Down
18 changes: 18 additions & 0 deletions ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ class ObservableTunnel internal constructor(

suspend fun deleteAsync() = manager.delete(this)

suspend fun setShortcutsAsync(hasShortcuts: Boolean) = manager.setShortcuts(this, hasShortcuts)

@get:Bindable
var hasShortcut: Boolean? = null
get() {
if (field == null)
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
applicationScope.launch {
try {
field = manager.hasShortcut(this@ObservableTunnel)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
return field
}
private set


companion object {
private const val TAG = "WireGuard/ObservableTunnel"
Expand Down
55 changes: 55 additions & 0 deletions ui/src/main/java/com/wireguard/android/model/ShortcutManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.wireguard.android.model

import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.wireguard.android.BuildConfig
import com.wireguard.android.R
import com.wireguard.android.activity.TunnelToggleActivity

class ShortcutManager(private val context: Context) {

private fun upIdFor(name: String): String = "$name-UP"
private fun downIdFor(name: String): String = "$name-DOWN"

private fun createShortcutIntent(action: String, tunnelName: String): Intent =
Intent(context, TunnelToggleActivity::class.java).apply {
setPackage(BuildConfig.APPLICATION_ID)
setAction(action)
putExtra("tunnel", tunnelName)
}

fun addShortcuts(name: String) {
val upIntent = createShortcutIntent("com.wireguard.android.action.SET_TUNNEL_UP", name)
val shortcutUp = ShortcutInfoCompat.Builder(context, upIdFor(name))
.setShortLabel(context.getString(R.string.shortcut_label_short_up, name))
.setLongLabel(context.getString(R.string.shortcut_label_long_up, name))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_baseline_arrow_circle_up_24))
.setIntent(upIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcutUp)

val downIntent = createShortcutIntent("com.wireguard.android.action.SET_TUNNEL_DOWN", name)
val shortcutDown = ShortcutInfoCompat.Builder(context, downIdFor(name))
.setShortLabel(context.getString(R.string.shortcut_label_short_down, name))
.setLongLabel(context.getString(R.string.shortcut_label_long_down, name))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_baseline_arrow_circle_down_24))
.setIntent(downIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcutDown)
}

fun removeShortcuts(name: String) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(upIdFor(name), downIdFor(name)))
}

fun hasShortcut(name: String) =
ShortcutManagerCompat.getDynamicShortcuts(context).any { it.id.startsWith(name) }
}
26 changes: 26 additions & 0 deletions ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
private val context: Context = get()
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
private val shortcutManager = ShortcutManager(context)
private var haveLoaded = false

private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
Expand All @@ -65,6 +66,10 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
// Make sure nothing touches the tunnel.
if (wasLastUsed)
lastUsedTunnel = null
// Make sure we also remove any existing shortcuts.
if (shortcutManager.hasShortcut(tunnel.name)) {
shortcutManager.removeShortcuts(tunnel.name)
}
tunnelMap.remove(tunnel)
try {
if (originalState == Tunnel.State.UP)
Expand Down Expand Up @@ -169,6 +174,11 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
// Make sure nothing touches the tunnel.
if (wasLastUsed)
lastUsedTunnel = null
// Make sure we also remove any existing shortcuts. We will add them back with the new name.
val hadShortcuts = shortcutManager.hasShortcut(tunnel.name)
if (hadShortcuts) {
shortcutManager.removeShortcuts(tunnel.name)
}
tunnelMap.remove(tunnel)
var throwable: Throwable? = null
var newName: String? = null
Expand All @@ -188,6 +198,10 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
// Add back previous shortcuts with the new name (if any).
if (hadShortcuts) {
shortcutManager.addShortcuts(tunnel.name)
}
if (throwable != null)
throw throwable
newName!!
Expand All @@ -210,6 +224,18 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
newState
}

suspend fun setShortcuts(tunnel: ObservableTunnel, hasShortcuts: Boolean) = withContext(Dispatchers.Main.immediate) {
if (hasShortcuts) {
shortcutManager.addShortcuts(tunnel.name)
} else {
shortcutManager.removeShortcuts(tunnel.name)
}
}

suspend fun hasShortcut(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
return@withContext shortcutManager.hasShortcut(tunnel.name)
}

class IntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
applicationScope.launch {
Expand Down
22 changes: 22 additions & 0 deletions ui/src/main/res/drawable/avd_star_to_border.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!--
~ Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->

<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_baseline_star">

<target android:name="star">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="300"
android:valueFrom="@string/vdpath_star_full"
android:valueTo="@string/vdpath_star_border"
android:valueType="pathType"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
</aapt:attr>
</target>
</animated-vector>
22 changes: 22 additions & 0 deletions ui/src/main/res/drawable/avd_star_to_full.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!--
~ Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->

<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_baseline_star_border">

<target android:name="star">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="300"
android:valueFrom="@string/vdpath_star_border"
android:valueTo="@string/vdpath_star_full"
android:valueType="pathType"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
</aapt:attr>
</target>
</animated-vector>
26 changes: 26 additions & 0 deletions ui/src/main/res/drawable/ic_action_shortcut.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->

<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/star_full"
android:drawable="@drawable/ic_baseline_star"
android:state_checked="true"/>
<item
android:id="@+id/star_border"
android:drawable="@drawable/ic_baseline_star_border"
android:state_checked="false"/>

<transition
android:drawable="@drawable/avd_star_to_full"
android:fromId="@id/star_border"
android:toId="@id/star_full"/>

<transition
android:drawable="@drawable/avd_star_to_border"
android:fromId="@id/star_full"
android:toId="@id/star_border"/>

</animated-selector>
5 changes: 5 additions & 0 deletions ui/src/main/res/drawable/ic_baseline_arrow_circle_down_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</vector>
5 changes: 5 additions & 0 deletions ui/src/main/res/drawable/ic_baseline_arrow_circle_up_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20M12,22c5.52,0 10,-4.48 10,-10c0,-5.52 -4.48,-10 -10,-10C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22L12,22zM11,12l0,4h2l0,-4h3l-4,-4l-4,4H11z"/>
</vector>
17 changes: 17 additions & 0 deletions ui/src/main/res/drawable/ic_baseline_star.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!--
~ Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24" >
<path
android:name="star"
android:fillColor="@android:color/white"
android:pathData="@string/vdpath_star_full"/>
</vector>
16 changes: 16 additions & 0 deletions ui/src/main/res/drawable/ic_baseline_star_border.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--
~ Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:tint="#000000" >
<path
android:name="star"
android:fillColor="@android:color/white"
android:pathData="@string/vdpath_star_border"/>
</vector>
Loading