fix: improve am config edit and import

Fixes bugs with new ip config fields

Improves performance of config edit screen

fixes nightly run workflows
This commit is contained in:
Zane Schepke 2024-09-20 19:46:01 -04:00
parent 7fee0b3768
commit d7b4fbecb9
22 changed files with 421 additions and 750 deletions

View File

@ -1,21 +0,0 @@
name: check-date
on:
workflow_dispatch:
workflow_call:
jobs:
check_date:
runs-on: ubuntu-latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v2
- name: print latest_commit
run: echo ${{ github.sha }}
- id: should_run
continue-on-error: true
name: check latest commit is less than a day
if: ${{ github.event_name == 'schedule' }}
run: test -z $(git rev-list --after="23 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"

View File

@ -1,19 +0,0 @@
name: nightly
on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch:
jobs:
check-date:
uses: ./.github/workflows/check-date.yml
nightly-publish-check:
name: Nightly publish check
runs-on: ubuntu-latest
needs: check-date
if: ${{ needs.check-date.outputs.should_run != 'false' }}
publish-nightly:
name: Publish nightly
runs-on: ubuntu-latest
needs: nightly-publish-check
uses: ./.github/workflows/release.yml

View File

@ -1,6 +1,8 @@
name: release-android name: release-android
on: on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
track: track:
@ -31,10 +33,25 @@ on:
workflow_call: workflow_call:
jobs: jobs:
check_date:
runs-on: ubuntu-latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v4
- name: print latest_commit
run: echo ${{ github.sha }}
- id: should_run
continue-on-error: true
name: check latest commit is less than a day
if: ${{ github.event_name == 'schedule' }}
run: test -z $(git rev-list --after="23 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
build: build:
needs: check_date
if: ${{ (needs.check_date.outputs.should_run != 'false' && github.event_name == 'schedule') || github.event_name != 'schedule'}}
name: Build Signed APK name: Build Signed APK
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
@ -47,7 +64,7 @@ jobs:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
steps: steps:
- uses: actions/checkout@4 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:

View File

@ -164,8 +164,6 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt // hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)

View File

@ -58,6 +58,11 @@ data class TunnelConfig(
) )
var pingIp: String? = null, var pingIp: String? = null,
) { ) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
}
companion object { companion object {
fun configFromWgQuick(wgQuick: String): Config { fun configFromWgQuick(wgQuick: String): Config {

View File

@ -41,6 +41,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
@ -179,16 +180,14 @@ class MainActivity : AppCompatActivity() {
), ),
) { ) {
val id = it.arguments?.getString("id") val id = it.arguments?.getString("id")
val configType =
ConfigType.valueOf(
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
)
if (!id.isNullOrBlank()) { if (!id.isNullOrBlank()) {
val viewModel = hiltViewModel<ConfigViewModel, ConfigViewModel.ConfigViewModelFactory> { factory ->
factory.create(id.toInt())
}
ConfigScreen( ConfigScreen(
navController = navController, viewModel = viewModel,
tunnelId = id,
focusRequester = focusRequester, focusRequester = focusRequester,
configType = configType, tunnelId = id.toInt(),
) )
} }
} }

View File

@ -18,7 +18,7 @@ fun ConfigurationToggle(
enabled: Boolean = true, enabled: Boolean = true,
checked: Boolean, checked: Boolean,
padding: Dp, padding: Dp,
onCheckChanged: () -> Unit, onCheckChanged: (checked: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
@ -44,7 +44,7 @@ fun ConfigurationToggle(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
checked = checked, checked = checked,
onCheckedChange = { onCheckChanged() }, onCheckedChange = { onCheckChanged(it) },
) )
} }
} }

View File

@ -10,6 +10,8 @@ import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -23,8 +25,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.core.text.isDigitsOnly
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
@ -45,36 +45,31 @@ fun SubmitConfigurationTextBox(
val isFocused by interactionSource.collectIsFocusedAsState() val isFocused by interactionSource.collectIsFocusedAsState()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
var stateValue by remember { mutableStateOf(value) } var stateValue by remember { mutableStateOf(value ?: "") }
ConfigurationTextBox( OutlinedTextField(
isError = isErrorValue(stateValue), isError = isErrorValue(stateValue),
interactionSource = interactionSource,
value = stateValue ?: "",
onValueChange = {
when (keyboardOptions.keyboardType) {
KeyboardType.Number -> {
if (it.isDigitsOnly()) stateValue = it
}
else -> stateValue = it
}
},
keyboardOptions = keyboardOptions,
label = label,
keyboardActions = KeyboardActions(
onDone = {
onSubmit(stateValue!!)
keyboardController?.hide()
},
),
hint = hint,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester), .focusRequester(focusRequester),
trailing = { value = stateValue,
if (!stateValue.isNullOrBlank() && !isErrorValue(stateValue) && isFocused) { singleLine = true,
interactionSource = interactionSource,
onValueChange = { stateValue = it },
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint) },
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(
onDone = {
onSubmit(stateValue)
keyboardController?.hide()
},
),
trailingIcon = {
if (!isErrorValue(stateValue) && isFocused) {
IconButton(onClick = { IconButton(onClick = {
onSubmit(stateValue!!) onSubmit(stateValue)
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
}) { }) {

View File

@ -42,7 +42,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -53,7 +52,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -65,21 +63,17 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -88,13 +82,7 @@ import kotlinx.coroutines.delay
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
) )
@Composable @Composable
fun ConfigScreen( fun ConfigScreen(tunnelId: Int, viewModel: ConfigViewModel, focusRequester: FocusRequester) {
viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester,
navController: NavController,
tunnelId: String,
configType: ConfigType,
) {
val context = LocalContext.current val context = LocalContext.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@ -102,12 +90,11 @@ fun ConfigScreen(
var showApplicationsDialog by remember { mutableStateOf(false) } var showApplicationsDialog by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
var isAuthenticated by remember { mutableStateOf(false) } var isAuthenticated by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.init(tunnelId) } LaunchedEffect(Unit) {
LaunchedEffect(uiState.loading) {
if (!uiState.loading && context.isRunningOnTv()) { if (!uiState.loading && context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching { kotlin.runCatching {
@ -119,13 +106,7 @@ fun ConfigScreen(
} }
} }
if (uiState.loading) {
LoadingScreen()
return
}
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
val fillMaxHeight = .85f val fillMaxHeight = .85f
@ -329,27 +310,11 @@ fun ConfigScreen(
Scaffold( Scaffold(
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( FloatingActionButton(
modifier =
Modifier.onFocusChanged {
if (context.isRunningOnTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
onClick = { onClick = {
viewModel.onSaveAllChanges(configType).onSuccess { viewModel.onSaveAllChanges()
snackbar.showMessage(
context.getString(R.string.config_changes_saved),
)
navController.navigate(Screen.Main.route)
}.onFailure {
snackbar.showMessage(it.getMessage(context))
}
}, },
containerColor = fobColor, containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { ) {
Icon( Icon(
@ -399,9 +364,16 @@ fun ConfigScreen(
stringResource(R.string.interface_), stringResource(R.string.interface_),
padding = screenPadding, padding = screenPadding,
) )
ConfigurationToggle(
stringResource(id = R.string.show_amnezia_properties),
checked = configType == ConfigType.AMNEZIA,
padding = screenPadding,
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
modifier = Modifier.focusRequester(focusRequester),
)
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.tunnelName, value = uiState.tunnelName,
onValueChange = { value -> viewModel.onTunnelNameChange(value) }, onValueChange = viewModel::onTunnelNameChange,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.name), label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
@ -417,12 +389,12 @@ fun ConfigScreen(
.clickable { showAuthPrompt = true }, .clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey, value = uiState.interfaceProxy.privateKey,
visualTransformation = visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) { if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated) {
VisualTransformation.None VisualTransformation.None
} else { } else {
PasswordVisualTransformation() PasswordVisualTransformation()
}, },
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = { trailingIcon = {
IconButton( IconButton(
@ -472,31 +444,29 @@ fun ConfigScreen(
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
) )
Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.addresses, value = uiState.interfaceProxy.addresses,
onValueChange = { value -> viewModel.onAddressesChanged(value) }, onValueChange = viewModel::onAddressesChanged,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.addresses), label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(3 / 5f) .fillMaxWidth()
.padding(end = 5.dp), .padding(end = 5.dp),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort, value = uiState.interfaceProxy.listenPort,
onValueChange = { value -> viewModel.onListenPortChanged(value) }, onValueChange = viewModel::onListenPortChanged,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.listen_port), label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random), hint = stringResource(R.string.random),
modifier = Modifier.width(IntrinsicSize.Min), modifier = Modifier.fillMaxWidth(),
) )
}
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.dnsServers, value = uiState.interfaceProxy.dnsServers,
onValueChange = { value -> viewModel.onDnsServersChanged(value) }, onValueChange = viewModel::onDnsServersChanged,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers), label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
@ -507,7 +477,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.mtu, value = uiState.interfaceProxy.mtu,
onValueChange = { value -> viewModel.onMtuChanged(value) }, onValueChange = viewModel::onMtuChanged,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.mtu), label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto), hint = stringResource(R.string.auto),
@ -517,10 +487,7 @@ fun ConfigScreen(
if (configType == ConfigType.AMNEZIA) { if (configType == ConfigType.AMNEZIA) {
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount, value = uiState.interfaceProxy.junkPacketCount,
onValueChange = { onValueChange = viewModel::onJunkPacketCountChanged,
value ->
viewModel.onJunkPacketCountChanged(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count), label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(), hint = stringResource(R.string.junk_packet_count).lowercase(),
@ -531,11 +498,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize, value = uiState.interfaceProxy.junkPacketMinSize,
onValueChange = { value -> onValueChange = viewModel::onJunkPacketMinSizeChanged,
viewModel.onJunkPacketMinSizeChanged(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size), label = stringResource(R.string.junk_packet_minimum_size),
hint = hint =
@ -549,11 +512,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize, value = uiState.interfaceProxy.junkPacketMaxSize,
onValueChange = { value -> onValueChange = viewModel::onJunkPacketMaxSizeChanged,
viewModel.onJunkPacketMaxSizeChanged(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size), label = stringResource(R.string.junk_packet_maximum_size),
hint = hint =
@ -567,11 +526,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize, value = uiState.interfaceProxy.initPacketJunkSize,
onValueChange = { value -> onValueChange = viewModel::onInitPacketJunkSizeChanged,
viewModel.onInitPacketJunkSizeChanged(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size), label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(), hint = stringResource(R.string.init_packet_junk_size).lowercase(),
@ -582,10 +537,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize, value = uiState.interfaceProxy.responsePacketJunkSize,
onValueChange = { onValueChange = viewModel::onResponsePacketJunkSize,
value ->
viewModel.onResponsePacketJunkSize(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size), label = stringResource(R.string.response_packet_junk_size),
hint = hint =
@ -599,10 +551,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader, value = uiState.interfaceProxy.initPacketMagicHeader,
onValueChange = { onValueChange = viewModel::onInitPacketMagicHeader,
value ->
viewModel.onInitPacketMagicHeader(value)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header), label = stringResource(R.string.init_packet_magic_header),
hint = hint =
@ -616,11 +565,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader, value = uiState.interfaceProxy.responsePacketMagicHeader,
onValueChange = { value -> onValueChange = viewModel::onResponsePacketMagicHeader,
viewModel.onResponsePacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header), label = stringResource(R.string.response_packet_magic_header),
hint = hint =
@ -634,11 +579,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader, value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = { value -> onValueChange = viewModel::onUnderloadPacketMagicHeader,
viewModel.onUnderloadPacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header), label = stringResource(R.string.underload_packet_magic_header),
hint = hint =
@ -652,11 +593,7 @@ fun ConfigScreen(
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader, value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value -> onValueChange = viewModel::onTransportPacketMagicHeader,
viewModel.onTransportPacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header), label = stringResource(R.string.transport_packet_magic_header),
hint = hint =
@ -814,9 +751,6 @@ fun ConfigScreen(
} }
} }
} }
if (context.isRunningOnTv()) {
Spacer(modifier = Modifier.weight(.17f))
}
} }
} }
} }

View File

@ -15,7 +15,7 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false, val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true, val loading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val tunnelName: String = "", var tunnelName: String = "",
val isAmneziaEnabled: Boolean = false, val isAmneziaEnabled: Boolean = false,
) { ) {
companion object { companion object {
@ -45,7 +45,6 @@ data class ConfigUiState(
} }
fun from(config: org.amnezia.awg.config.Config): ConfigUiState { fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
// TODO update with new values
val proxyPeers = config.peers.map { PeerProxy.from(it) } val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`) val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true var include = true
@ -69,5 +68,13 @@ data class ConfigUiState(
isAllApplicationsEnabled, isAllApplicationsEnabled,
) )
} }
fun from(tunnel: TunnelConfig): ConfigUiState {
val config = tunnel.toAmConfig()
return from(config).copy(
tunnelName = tunnel.name,
tunnel = tunnel,
)
}
} }
} }

View File

@ -6,6 +6,7 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.wireguard.config.Config import com.wireguard.config.Config
import com.wireguard.config.Interface import com.wireguard.config.Interface
import com.wireguard.config.Peer import com.wireguard.config.Peer
@ -15,103 +16,83 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.extensions.removeAt import com.zaneschepke.wireguardautotunnel.util.extensions.removeAt
import com.zaneschepke.wireguardautotunnel.util.extensions.update import com.zaneschepke.wireguardautotunnel.util.extensions.update
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
@HiltViewModel @HiltViewModel(assistedFactory = ConfigViewModel.ConfigViewModelFactory::class)
class ConfigViewModel class ConfigViewModel
@Inject @AssistedInject
constructor( constructor(
private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val navController: NavHostController,
@Assisted val id: Int,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val packageManager = WireGuardAutoTunnel.instance.packageManager private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.onStart {
appDataRepository.tunnels.getById(id)?.let {
fun init(tunnelId: String) = viewModelScope.launch(ioDispatcher) { _uiState.value = ConfigUiState.from(it)
val packages = getQueriedPackages("") }
val state = }.stateIn(
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { viewModelScope + ioDispatcher,
val tunnelConfig = SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
appDataRepository.tunnels.getAll() ConfigUiState(),
.firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) {
(
if (isAmneziaEnabled) {
val amConfig =
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
} else {
ConfigUiState.from(
TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick),
) )
}
).copy(
packages = packages,
loading = false,
tunnel = tunnelConfig,
tunnelName = tunnelConfig.name,
isAmneziaEnabled = isAmneziaEnabled,
)
} else {
ConfigUiState(loading = false, packages = packages)
}
} else {
ConfigUiState(loading = false, packages = packages)
}
_uiState.value = state
}
fun onTunnelNameChange(name: String) { fun onTunnelNameChange(name: String) {
_uiState.value = _uiState.value.copy(tunnelName = name) _uiState.update {
it.copy(tunnelName = name)
}
} }
fun onIncludeChange(include: Boolean) { fun onIncludeChange(include: Boolean) {
_uiState.value = _uiState.value.copy(include = include) _uiState.update {
it.copy(include = include)
}
} }
fun onAddCheckedPackage(packageName: String) { fun onAddCheckedPackage(packageName: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
checkedPackageNames = _uiState.value.checkedPackageNames + packageName, checkedPackageNames = it.checkedPackageNames + packageName,
) )
} }
}
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled) _uiState.update {
it.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
}
} }
fun onRemoveCheckedPackage(packageName: String) { fun onRemoveCheckedPackage(packageName: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
checkedPackageNames = _uiState.value.checkedPackageNames - packageName, checkedPackageNames = it.checkedPackageNames - packageName,
) )
} }
private fun getQueriedPackages(query: String): List<PackageInfo> {
return getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase())
}
} }
fun getPackageLabel(packageInfo: PackageInfo): String { fun getPackageLabel(packageInfo: PackageInfo): String {
@ -137,7 +118,9 @@ constructor(
return _uiState.value.isAllApplicationsEnabled return _uiState.value.isAllApplicationsEnabled
} }
private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch { private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
@ -174,21 +157,24 @@ constructor(
} }
private fun emptyCheckedPackagesList() { private fun emptyCheckedPackagesList() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) _uiState.update {
it.copy(checkedPackageNames = emptyList())
}
} }
private fun buildInterfaceListFromProxyInterface(): Interface { private fun buildInterfaceListFromProxyInterface(): Interface {
val builder = Interface.Builder() val builder = Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) with(_uiState.value.interfaceProxy) {
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) builder.parsePrivateKey(this.privateKey.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { builder.parseAddresses(this.addresses.trim())
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) if (this.dnsServers.isNotEmpty()) {
builder.parseDnsServers(this.dnsServers.trim())
} }
if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) { if (this.mtu.isNotEmpty()) {
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) builder.parseMtu(this.mtu.trim())
} }
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { if (this.listenPort.isNotEmpty()) {
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) builder.parseListenPort(this.listenPort.trim())
} }
if (isAllApplicationsEnabled()) emptyCheckedPackagesList() if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) { if (_uiState.value.include) {
@ -201,21 +187,24 @@ constructor(
_uiState.value.checkedPackageNames, _uiState.value.checkedPackageNames,
) )
} }
}
return builder.build() return builder.build()
} }
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
val builder = org.amnezia.awg.config.Interface.Builder() val builder = org.amnezia.awg.config.Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) with(_uiState.value.interfaceProxy) {
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) builder.parsePrivateKey(this.privateKey.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { builder.parseAddresses(this.addresses.trim())
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) if (this.dnsServers.isNotEmpty()) {
builder.parseDnsServers(this.dnsServers.trim())
} }
if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) { if (this.mtu.isNotEmpty()) {
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) builder.parseMtu(this.mtu.trim())
} }
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { if (this.listenPort.isNotEmpty()) {
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) builder.parseListenPort(this.listenPort.trim())
} }
if (isAllApplicationsEnabled()) emptyCheckedPackagesList() if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) { if (_uiState.value.include) {
@ -228,51 +217,53 @@ constructor(
_uiState.value.checkedPackageNames, _uiState.value.checkedPackageNames,
) )
} }
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { if (this.junkPacketCount.isNotEmpty()) {
builder.setJunkPacketCount( builder.setJunkPacketCount(
_uiState.value.interfaceProxy.junkPacketCount.trim().toInt(), this.junkPacketCount.trim().toInt(),
) )
} }
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { if (this.junkPacketMinSize.isNotEmpty()) {
builder.setJunkPacketMinSize( builder.setJunkPacketMinSize(
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(), this.junkPacketMinSize.trim().toInt(),
) )
} }
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { if (this.junkPacketMaxSize.isNotEmpty()) {
builder.setJunkPacketMaxSize( builder.setJunkPacketMaxSize(
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(), this.junkPacketMaxSize.trim().toInt(),
) )
} }
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { if (this.initPacketJunkSize.isNotEmpty()) {
builder.setInitPacketJunkSize( builder.setInitPacketJunkSize(
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(), this.initPacketJunkSize.trim().toInt(),
) )
} }
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { if (this.responsePacketJunkSize.isNotEmpty()) {
builder.setResponsePacketJunkSize( builder.setResponsePacketJunkSize(
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(), this.responsePacketJunkSize.trim().toInt(),
) )
} }
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { if (this.initPacketMagicHeader.isNotEmpty()) {
builder.setInitPacketMagicHeader( builder.setInitPacketMagicHeader(
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(), this.initPacketMagicHeader.trim().toLong(),
) )
} }
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { if (this.responsePacketMagicHeader.isNotEmpty()) {
builder.setResponsePacketMagicHeader( builder.setResponsePacketMagicHeader(
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(), this.responsePacketMagicHeader.trim().toLong(),
) )
} }
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { if (this.transportPacketMagicHeader.isNotEmpty()) {
builder.setTransportPacketMagicHeader( builder.setTransportPacketMagicHeader(
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(), this.transportPacketMagicHeader.trim().toLong(),
) )
} }
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { if (this.underloadPacketMagicHeader.isNotEmpty()) {
builder.setUnderloadPacketMagicHeader( builder.setUnderloadPacketMagicHeader(
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(), this.underloadPacketMagicHeader.trim().toLong(),
) )
} }
}
return builder.build() return builder.build()
} }
@ -291,41 +282,32 @@ constructor(
.build() .build()
} }
fun onSaveAllChanges(configType: ConfigType): Result<Unit> { fun onSaveAllChanges() = viewModelScope.launch {
return try { kotlin.runCatching {
val wgQuick = buildConfig().toWgQuickString(true) val wgQuick = buildConfig().toWgQuickString(true)
val amQuick = val amQuick = buildAmConfig().toAwgQuickString(true)
if (configType == ConfigType.AMNEZIA) { val tunnelConfig = uiState.value.tunnel?.copy(
buildAmConfig().toAwgQuickString(true) name = _uiState.value.tunnelName,
} else { amQuick = amQuick,
TunnelConfig.AM_QUICK_DEFAULT wgQuick = wgQuick,
} ) ?: TunnelConfig(
val tunnelConfig =
when (uiState.value.tunnel) {
null ->
TunnelConfig(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = wgQuick, wgQuick = wgQuick,
amQuick = amQuick, amQuick = amQuick,
) )
else ->
uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName,
wgQuick = wgQuick,
amQuick = amQuick,
)
}
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.success(Unit) SnackbarController.showMessage(
} catch (e: Exception) { StringValue.StringResource(R.string.config_changes_saved),
Timber.e(e) )
val message = e.message?.substringAfter(":", missingDelimiterValue = "") navController.navigate(Screen.Main.route)
}.onFailure {
Timber.e(it)
val message = it.message?.substringAfter(":", missingDelimiterValue = "")
val stringValue = val stringValue =
message?.let { message?.let {
StringValue.DynamicString(message) StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error) } ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue)) SnackbarController.showMessage(stringValue)
} }
} }
@ -408,7 +390,7 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(), publicKey = keyPair.publicKey.toBase64(),
), ),
@ -419,7 +401,7 @@ constructor(
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), interfaceProxy = it.interfaceProxy.copy(addresses = value),
) )
} }
} }
@ -427,7 +409,7 @@ constructor(
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), interfaceProxy = it.interfaceProxy.copy(listenPort = value),
) )
} }
} }
@ -435,21 +417,21 @@ constructor(
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), interfaceProxy = it.interfaceProxy.copy(dnsServers = value),
) )
} }
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
_uiState.update { _uiState.update {
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) it.copy(interfaceProxy = it.interfaceProxy.copy(mtu = value))
} }
} }
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), interfaceProxy = it.interfaceProxy.copy(publicKey = value),
) )
} }
} }
@ -457,7 +439,7 @@ constructor(
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), interfaceProxy = it.interfaceProxy.copy(privateKey = value),
) )
} }
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
@ -479,7 +461,7 @@ constructor(
fun onJunkPacketCountChanged(value: String) { fun onJunkPacketCountChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value), interfaceProxy = it.interfaceProxy.copy(junkPacketCount = value),
) )
} }
} }
@ -487,7 +469,7 @@ constructor(
fun onJunkPacketMinSizeChanged(value: String) { fun onJunkPacketMinSizeChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value), interfaceProxy = it.interfaceProxy.copy(junkPacketMinSize = value),
) )
} }
} }
@ -495,7 +477,7 @@ constructor(
fun onJunkPacketMaxSizeChanged(value: String) { fun onJunkPacketMaxSizeChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value), interfaceProxy = it.interfaceProxy.copy(junkPacketMaxSize = value),
) )
} }
} }
@ -503,7 +485,7 @@ constructor(
fun onInitPacketJunkSizeChanged(value: String) { fun onInitPacketJunkSizeChanged(value: String) {
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value), interfaceProxy = it.interfaceProxy.copy(initPacketJunkSize = value),
) )
} }
} }
@ -512,7 +494,7 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
responsePacketJunkSize = value, responsePacketJunkSize = value,
), ),
) )
@ -523,7 +505,7 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
initPacketMagicHeader = value, initPacketMagicHeader = value,
), ),
) )
@ -534,7 +516,7 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
responsePacketMagicHeader = value, responsePacketMagicHeader = value,
), ),
) )
@ -545,7 +527,7 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
transportPacketMagicHeader = value, transportPacketMagicHeader = value,
), ),
) )
@ -556,10 +538,15 @@ constructor(
_uiState.update { _uiState.update {
it.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( it.interfaceProxy.copy(
underloadPacketMagicHeader = value, underloadPacketMagicHeader = value,
), ),
) )
} }
} }
@AssistedFactory
interface ConfigViewModelFactory {
fun create(id: Int): ConfigViewModel
}
} }

View File

@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll import androidx.compose.material.icons.rounded.CopyAll
@ -29,6 +30,7 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -73,20 +75,18 @@ import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -99,7 +99,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
var showVpnPermissionDialog by remember { mutableStateOf(false) } var showVpnPermissionDialog by remember { mutableStateOf(false) }
val isVisible = rememberSaveable { mutableStateOf(true) } val isVisible = rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
@ -152,11 +151,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
context.getString(R.string.error_no_file_explorer), context.getString(R.string.error_no_file_explorer),
) )
}, onData = { data -> }, onData = { data ->
scope.launch { viewModel.onTunnelFileSelected(data, context)
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
snackbar.showMessage(it.getMessage(context))
}
}
}) })
val scanLauncher = val scanLauncher =
@ -164,11 +159,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
contract = ScanContract(), contract = ScanContract(),
onResult = { onResult = {
if (it.contents != null) { if (it.contents != null) {
scope.launch { viewModel.onTunnelQrResult(it.contents)
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
snackbar.showMessage(error.getMessage(context))
}
}
} }
}, },
) )
@ -227,9 +218,15 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
ScrollDismissMultiFab(R.drawable.add, focusRequester, isVisible = isVisible.value, onFabItemClicked = { ScrollDismissFab(icon = {
val icon = Icons.Filled.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, focusRequester, isVisible = isVisible.value, onClick = {
showBottomSheet = true showBottomSheet = true
configType = ConfigType.valueOf(it.value)
}) })
}, },
) { ) {
@ -240,7 +237,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
onQrClick = { launchQrScanner() }, onQrClick = { launchQrScanner() },
onManualImportClick = { onManualImportClick = {
navController.navigate( navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType", "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
) )
}, },
) )

View File

@ -6,16 +6,19 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileReadException
import com.zaneschepke.wireguardautotunnel.util.InvalidFileExtensionException
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -70,32 +73,17 @@ constructor(
tunnelService.stopTunnel(tunnel) tunnelService.stopTunnel(tunnel)
} }
private fun validateConfigString(config: String, configType: ConfigType) { private fun generateQrCodeDefaultName(config: String): String {
when (configType) {
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
}
}
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
return try { return try {
when (configType) {
ConfigType.AMNEZIA -> {
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
}
ConfigType.WIREGUARD -> {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
}
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
NumberUtils.generateRandomTunnelName() NumberUtils.generateRandomTunnelName()
} }
} }
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { private fun generateQrCodeTunnelName(config: String): String {
var defaultName = generateQrCodeDefaultName(config, configType) var defaultName = generateQrCodeDefaultName(config)
val lines = config.lines().toMutableList() val lines = config.lines().toMutableList()
val linesIterator = lines.iterator() val linesIterator = lines.iterator()
while (linesIterator.hasNext()) { while (linesIterator.hasNext()) {
@ -108,37 +96,18 @@ constructor(
return defaultName return defaultName
} }
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> { fun onTunnelQrResult(result: String) = viewModelScope.launch(ioDispatcher) {
return withContext(ioDispatcher) { kotlin.runCatching {
try { val amConfig = TunnelConfig.configFromAmQuick(result)
validateConfigString(result, configType) val amQuick = amConfig.toAwgQuickString(true)
val tunnelName = val wgQuick = amConfig.toWgQuickString()
makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig =
when (configType) {
ConfigType.AMNEZIA -> {
TunnelConfig(
name = tunnelName,
amQuick = result,
wgQuick =
TunnelConfig.configFromAmQuick(
result,
).toWgQuickString(),
)
}
ConfigType.WIREGUARD -> val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result))
TunnelConfig( val tunnelConfig = TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick)
name = tunnelName, saveTunnel(tunnelConfig)
wgQuick = result, }.onFailure {
) Timber.e(it)
} SnackbarController.showMessage(StringValue.StringResource(R.string.error_invalid_code))
addTunnel(tunnelConfig)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.InvalidQrCode())
}
} }
} }
@ -155,130 +124,70 @@ constructor(
} }
} }
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) { private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
var amQuick: String? = null val amConfig = stream.use { org.amnezia.awg.config.Config.parse(it) }
val wgQuick =
stream.use {
when (type) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString(true)
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(it).toWgQuickString(true)
}
}
}
viewModelScope.launch {
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName)) val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel( saveTunnel(
TunnelConfig( TunnelConfig(
name = tunnelName, name = tunnelName,
wgQuick = wgQuick, wgQuick = amConfig.toWgQuickString(),
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, amQuick = amConfig.toAwgQuickString(true),
), ),
) )
} }
}
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? { private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return context.applicationContext.contentResolver.openInputStream(uri) return context.applicationContext.contentResolver.openInputStream(uri)
} }
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> { fun onTunnelFileSelected(uri: Uri, context: Context) = viewModelScope.launch(ioDispatcher) {
return withContext(ioDispatcher) { kotlin.runCatching {
try { if (!isValidUriContentScheme(uri)) throw InvalidFileExtensionException
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri) val fileName = getFileName(context, uri)
return@withContext when (getFileExtensionFromFileName(fileName)) { when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION -> Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context) saveTunnelFromConfUri(fileName, uri, context)
Constants.ZIP_FILE_EXTENSION -> Constants.ZIP_FILE_EXTENSION ->
saveTunnelsFromZipUri( saveTunnelsFromZipUri(
uri, uri,
configType,
context, context,
) )
else -> throw InvalidFileExtensionException
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
} }
}.onFailure {
Timber.e(it)
if (it is InvalidFileExtensionException) {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_file_extension))
} else { } else {
Result.failure(WgTunnelExceptions.InvalidFileExtension()) SnackbarController.showMessage(StringValue.StringResource(R.string.error_file_format))
}
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
} }
private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context): Result<Unit> { private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {
return withContext(ioDispatcher) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
.filterNot { .filterNot {
it.isDirectory || it.isDirectory ||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
} }
.forEach { .forEach { entry ->
val name = getNameFromFileName(it.name) val name = getNameFromFileName(entry.name)
withContext(viewModelScope.coroutineContext) { val amConf = org.amnezia.awg.config.Config.parse(zip.bufferedReader())
try { saveTunnel(
var amQuick: String? = null
val wgQuick =
when (configType) {
ConfigType.AMNEZIA -> {
val config =
org.amnezia.awg.config.Config.parse(
zip,
)
amQuick = config.toAwgQuickString(true)
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString(true)
}
}
addTunnel(
TunnelConfig( TunnelConfig(
name = makeTunnelNameUnique(name), name = makeTunnelNameUnique(name),
wgQuick = wgQuick, wgQuick = amConf.toWgQuickString(),
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT, amQuick = amConf.toAwgQuickString(true),
), ),
) )
Result.success(Unit)
} catch (e: Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
Result.success(Unit)
} }
} }
} }
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) {
return withContext(ioDispatcher) { val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException
val stream = getInputStreamFromUri(uri, context) saveTunnelConfigFromStream(stream, name)
return@withContext if (stream != null) {
try {
saveTunnelConfigFromStream(stream, name, configType)
} catch (e: Exception) {
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
}
Result.success(Unit)
} else {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
saveTunnel(tunnelConfig)
} }
fun pauseAutoTunneling() = viewModelScope.launch { fun pauseAutoTunneling() = viewModelScope.launch {
@ -300,32 +209,23 @@ constructor(
} }
private fun getFileNameByCursor(context: Context, uri: Uri): String? { private fun getFileNameByCursor(context: Context, uri: Uri): String? {
context.contentResolver.query(uri, null, null, null, null)?.use { return context.contentResolver.query(uri, null, null, null, null)?.use {
return getDisplayNameByCursor(it) getDisplayNameByCursor(it)
} }
return null
} }
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? { private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
return if (columnIndex != -1) { if (columnIndex == -1) return null
return columnIndex return columnIndex
} else {
null
}
} }
private fun getDisplayNameByCursor(cursor: Cursor): String? { private fun getDisplayNameByCursor(cursor: Cursor): String? {
return if (cursor.moveToFirst()) { val move = cursor.moveToFirst()
if (!move) return null
val index = getDisplayNameColumnIndex(cursor) val index = getDisplayNameColumnIndex(cursor)
if (index != null) { if (index == null) return index
cursor.getString(index) return cursor.getString(index)
} else {
null
}
} else {
null
}
} }
private fun isValidUriContentScheme(uri: Uri): Boolean { private fun isValidUriContentScheme(uri: Uri): Boolean {

View File

@ -1,41 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
@Composable @Composable
fun ScrollDismissMultiFab( fun ScrollDismissFab(icon: @Composable () -> Unit, focusRequester: FocusRequester, isVisible: Boolean, onClick: () -> Unit) {
@DrawableRes res: Int,
focusRequester: FocusRequester,
isVisible: Boolean,
onFabItemClicked: (fabItem: MultiFabItem) -> Unit,
) {
// Nested scroll for control FAB
val context = LocalContext.current
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
@ -45,64 +24,14 @@ fun ScrollDismissMultiFab(
.focusRequester(focusRequester) .focusRequester(focusRequester)
.focusGroup(), .focusGroup(),
) { ) {
val fobColor = MaterialTheme.colorScheme.secondary FloatingActionButton(
val fobIconColor = MaterialTheme.colorScheme.background onClick = {
MultiFloatingActionButton( onClick()
fabIcon =
FabIcon(
iconRes = res,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f,
),
fabOption =
FabOption(
iconTint = fobIconColor,
backgroundTint = fobColor,
),
itemsMultiFab =
listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
},
modifier =
Modifier
.size(40.dp),
icon = res,
value = ConfigType.AMNEZIA.name,
miniFabOption =
FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
MultiFabItem(
label = {
Text(
stringResource(id = R.string.wireguard),
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
},
icon = res,
value = ConfigType.WIREGUARD.name,
miniFabOption =
FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
),
onFabItemClicked = {
onFabItemClicked(it)
}, },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) containerColor = MaterialTheme.colorScheme.primary,
) {
icon()
}
} }
} }

View File

@ -19,6 +19,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -54,8 +55,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
@ -103,10 +103,16 @@ fun OptionsScreen(
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
ScrollDismissMultiFab(R.drawable.edit, focusRequester, isVisible = true, onFabItemClicked = { ScrollDismissFab(icon = {
val configType = ConfigType.valueOf(it.value) val icon = Icons.Filled.Edit
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, focusRequester, isVisible = true, onClick = {
navController.navigate( navController.navigate(
"${Screen.Config.route}/${config.id}?configType=${configType.name}", "${Screen.Config.route}/${config.id}",
) )
}) })
}, },
@ -279,10 +285,10 @@ fun OptionsScreen(
stringResource(R.string.set_custom_ping_ip), stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip), stringResource(R.string.default_ping_ip),
focusRequester, focusRequester,
isErrorValue = { !(it?.isValidIpv4orIpv6Address() ?: true) }, isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = { onSubmit = {
optionsViewModel.saveTunnelChanges( optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it), config.copy(pingIp = it.ifBlank { null }),
) )
}, },
) )
@ -301,7 +307,7 @@ fun OptionsScreen(
isErrorValue = ::isSecondsError, isErrorValue = ::isSecondsError,
onSubmit = { onSubmit = {
optionsViewModel.saveTunnelChanges( optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = it.toLong() * 1000), config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
) )
}, },
) )
@ -316,7 +322,7 @@ fun OptionsScreen(
isErrorValue = ::isSecondsError, isErrorValue = ::isSecondsError,
onSubmit = { onSubmit = {
optionsViewModel.saveTunnelChanges( optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = it.toLong() * 1000), config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
) )
}, },
) )

View File

@ -16,7 +16,6 @@ object Constants {
const val ZIP_FILE_MIME_TYPE = "application/zip" const val ZIP_FILE_MIME_TYPE = "application/zip"
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS" const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
const val EMAIL_MIME_TYPE = "plain/text" const val EMAIL_MIME_TYPE = "plain/text"
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024

View File

@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.util
object InvalidFileExtensionException : Exception() {
private fun readResolve(): Any = InvalidFileExtensionException
}
object FileReadException : Exception() {
private fun readResolve(): Any = FileReadException
}
object ConfigExportException : Exception() {
private fun readResolve(): Any = ConfigExportException
}

View File

@ -118,7 +118,7 @@ class FileUtils(
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed()) Result.failure(ConfigExportException)
} }
} }
} }

View File

@ -1,63 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
sealed class WgTunnelExceptions : Exception() {
abstract fun getMessage(context: Context): String
data class ConfigExportFailed(
private val userMessage: StringValue =
StringValue.StringResource(
R.string.export_configs_failed,
),
) : WgTunnelExceptions() {
override fun getMessage(context: Context): String {
return userMessage.asString(context)
}
}
data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) :
WgTunnelExceptions() {
override fun getMessage(context: Context): String {
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else ""
)
}
}
data class InvalidQrCode(
private val userMessage: StringValue =
StringValue.StringResource(
R.string.error_invalid_code,
),
) :
WgTunnelExceptions() {
override fun getMessage(context: Context): String {
return userMessage.asString(context)
}
}
data class InvalidFileExtension(
private val userMessage: StringValue =
StringValue.StringResource(
R.string.error_file_extension,
),
) : WgTunnelExceptions() {
override fun getMessage(context: Context): String {
return userMessage.asString(context)
}
}
data class FileReadFailed(
private val userMessage: StringValue =
StringValue.StringResource(
R.string.error_file_format,
),
) :
WgTunnelExceptions() {
override fun getMessage(context: Context): String {
return userMessage.asString(context)
}
}
}

View File

@ -1,11 +1,7 @@
package com.zaneschepke.wireguardautotunnel.util.extensions package com.zaneschepke.wireguardautotunnel.util.extensions
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import java.math.BigDecimal import java.math.BigDecimal
import java.text.DecimalFormat import java.text.DecimalFormat
@ -21,10 +17,3 @@ fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.rem
typealias TunnelConfigs = List<TunnelConfig> typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo> typealias Packages = List<PackageInfo>
fun Throwable.getMessage(context: Context): String {
return when (this) {
is WgTunnelExceptions -> this.getMessage(context)
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
}
}

View File

@ -194,4 +194,5 @@
<string name="set_custom_ping_cooldown">Ping restart cooldown (sec)</string> <string name="set_custom_ping_cooldown">Ping restart cooldown (sec)</string>
<string name="wildcard_supported">Learn about supported wildcards.</string> <string name="wildcard_supported">Learn about supported wildcards.</string>
<string name="details">details</string> <string name="details">details</string>
<string name="show_amnezia_properties">Show Amnezia properties</string>
</resources> </resources>

View File

@ -16,7 +16,6 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.7.2" kotlinx-serialization-json = "1.7.2"
lifecycle-runtime-compose = "2.8.6" lifecycle-runtime-compose = "2.8.6"
material3 = "1.3.0" material3 = "1.3.0"
multifabVersion = "1.1.1"
navigationCompose = "2.8.1" navigationCompose = "2.8.1"
pinLockCompose = "1.0.3" pinLockCompose = "1.0.3"
roomVersion = "2.6.1" roomVersion = "2.6.1"
@ -90,7 +89,6 @@ pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref =
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" } tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }