Skip to content

Commit

Permalink
feat: add quick setting tile
Browse files Browse the repository at this point in the history
Add quick settings tile for easy tunnel toggling and auto-tunnel override.

Fix bug on AndroidTV D-pad tunnel control for multiple tunnels.

Closes #18 , Closes #20
  • Loading branch information
zaneschepke committed Aug 31, 2023
1 parent f513297 commit 0e64bbb
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 24 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ android {

val versionMajor = 2
val versionMinor = 3
val versionPatch = 5
val versionPatch = 6
val versionBuild = 0

defaultConfig {
Expand Down Expand Up @@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")

// compose navigation
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")

// hilt
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:exported="true"
android:name=".service.TunnelControlTile"
android:icon="@drawable/shield"
android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.zaneschepke.wireguardautotunnel.service

import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class TunnelControlTile : TileService() {

@Inject
lateinit var settingsRepo : Repository<Settings>

@Inject
lateinit var configRepo : Repository<TunnelConfig>

@Inject
lateinit var vpnService : VpnService

private val scope = CoroutineScope(Dispatchers.Main);

private lateinit var job : Job

override fun onStartListening() {
if (!this::job.isInitialized) {
job = scope.launch {
updateTileState()
}
}
Timber.d("On start listening")
super.onStartListening()
}

override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = "Toggle VPN"
scope.launch {
updateTileState();
}
}

override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
}

override fun onClick() {
unlockAndRun {
scope.launch {
try {
if(vpnService.getState() == Tunnel.State.UP) {
stopTunnel();
return@launch
}
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
startTunnel(setting.defaultTunnel!!)
} else {
val config = configRepo.getAll()?.first();
if(config != null) {
startTunnel(config.toString());
}
}
}
} finally {
cancel()
}
}
super.onClick()
}
}

private fun stopTunnel() {
ServiceTracker.actionOnService(
Action.STOP, this@TunnelControlTile,
WireGuardConnectivityWatcherService::class.java)
ServiceTracker.actionOnService(
Action.STOP, this@TunnelControlTile,
WireGuardTunnelService::class.java)
}

private fun startTunnel(tunnelConfig : String) {
ServiceTracker.actionOnService(
Action.START, this.applicationContext,
WireGuardTunnelService::class.java,
mapOf(this.applicationContext.resources.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}

private suspend fun updateTileState() {
vpnService.state.collect {
when(it) {
Tunnel.State.UP -> {
setTileOn()
}
Tunnel.State.DOWN -> {
setTileOff()
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
qsTile.updateTile()
}
}

private fun setTileOff() {
qsTile.state = Tile.STATE_INACTIVE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = "Off"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = "VPN Off";
}
}

private fun setTileOn() {
qsTile.state = Tile.STATE_ACTIVE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = "On"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = "VPN On";
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class MainActivity : AppCompatActivity() {
}
}
}) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester)
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
}
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
Expand Down Expand Up @@ -85,7 +86,6 @@ import kotlinx.coroutines.launch
@Composable
fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState, navController: NavController
) {

Expand Down Expand Up @@ -149,7 +149,7 @@ fun MainScreen(
})
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
floatingActionButton = {
AnimatedVisibility(
visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }),
Expand Down Expand Up @@ -241,9 +241,12 @@ fun MainScreen(
.padding(padding)
) {

LazyColumn(modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),) {
items(tunnels.toList()) { tunnel ->
LazyColumn(
modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { index, tunnel ->
val focusRequester = FocusRequester();
RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint
Expand All @@ -263,15 +266,15 @@ fun MainScreen(
selectedTunnel = tunnel;
},
onClick = {
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)){
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
focusRequester.requestFocus()
}
},
},
rowButton = {
if (tunnel.id == selectedTunnel?.id) {
Row() {
Row {
IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) {
Expand All @@ -287,30 +290,43 @@ fun MainScreen(
}
}
} else {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)){
Row() {
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info")
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
}) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
Icon(
Icons.Rounded.Edit,
stringResource(id = R.string.edit)
)
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
viewModel.onDelete(tunnel)
}
viewModel.onDelete(tunnel)
}
}) {
Icon(
Icons.Rounded.Delete,
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/shield.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,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ buildscript {
}

plugins {
id("com.android.application") version "8.2.0-alpha15" apply false
id("com.android.application") version "8.2.0-beta01" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
kotlin("plugin.serialization") version "1.8.22" apply false
Expand Down

0 comments on commit 0e64bbb

Please sign in to comment.