feat: add quick setting tile
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
This commit is contained in:
parent
f513297ba0
commit
0e64bbb4e1
|
@ -17,7 +17,7 @@ android {
|
|||
|
||||
val versionMajor = 2
|
||||
val versionMinor = 3
|
||||
val versionPatch = 5
|
||||
val versionPatch = 6
|
||||
val versionBuild = 0
|
||||
|
||||
defaultConfig {
|
||||
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
@ -85,7 +86,6 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
|
||||
focusRequester: FocusRequester,
|
||||
snackbarHostState: SnackbarHostState, navController: NavController
|
||||
) {
|
||||
|
||||
|
@ -149,7 +149,7 @@ fun MainScreen(
|
|||
})
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
|
@ -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
|
||||
|
@ -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}")
|
||||
}) {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue