Skip to content

Commit

Permalink
Merge pull request #1 from gergely-sallai/dynamic-shortcuts
Browse files Browse the repository at this point in the history
Dynamic shortcuts
  • Loading branch information
nschwing authored Feb 17, 2023
2 parents 713947e + e065a21 commit 8ad2b05
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 6 deletions.
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.7.0'
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 @@ -72,6 +72,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

0 comments on commit 8ad2b05

Please sign in to comment.