Compare commits

..

3 Commits

Author SHA1 Message Date
Zane Schepke 594ed85a71 start sticky 2025-02-22 07:52:16 -05:00
Zane Schepke 13bb300f20 feat: add worker to restart killed services 2025-02-21 21:16:10 -05:00
Zane Schepke 1375a468fd feat: add export as amnezia or wg 2025-02-21 17:26:51 -05:00
14 changed files with 181 additions and 19 deletions
+6 -2
View File
@@ -172,8 +172,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
// tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -188,6 +187,7 @@ dependencies {
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
@@ -221,6 +221,10 @@ dependencies {
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
}
fun determineVersionName(): String {
+7
View File
@@ -106,6 +106,13 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
@@ -40,6 +40,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
@@ -134,6 +135,8 @@ class MainActivity : AppCompatActivity() {
}
}
ServiceWorker.start(this)
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
@@ -3,9 +3,11 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
@@ -26,7 +28,15 @@ import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
@Inject
@ApplicationScope
@@ -36,7 +36,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled) return@launch serviceManager.startAutoTunnel(true)
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
@@ -35,7 +35,7 @@ class BootReceiver : BroadcastReceiver() {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled) return@launch serviceManager.startAutoTunnel(true)
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
@@ -35,8 +35,9 @@ class TunnelForegroundService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
return super.onStartCommand(intent, flags, startId)
return START_NOT_STICKY
}
fun start(tunnelConf: TunnelConf) {
@@ -100,9 +100,10 @@ class AutoTunnelService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
return START_NOT_STICKY
}
fun start() {
@@ -0,0 +1,60 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType {
AMNEZIA,
WG,
}
@@ -5,8 +5,9 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
@@ -15,6 +16,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip
@@ -23,7 +25,11 @@ import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -38,6 +44,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route
@@ -56,7 +63,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(ExperimentalLayoutApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current
@@ -68,11 +75,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
val interactionSource = remember { MutableInteractionSource() }
var showAuthPrompt by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) }
if (showAuthPrompt) {
AuthorizationPrompt(
onSuccess = {
showAuthPrompt = false
viewModel.exportAllConfigs(context)
showExportSheet = true
},
onError = { _ ->
showAuthPrompt = false
@@ -89,6 +98,52 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
)
}
if (showExportSheet) {
ModalBottomSheet(onDismissRequest = { showExportSheet = false }) {
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
showExportSheet = false
viewModel.exportAllConfigs(context, ConfigType.AMNEZIA)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.FolderZip,
contentDescription = stringResource(id = R.string.export_amnezia),
modifier = Modifier.padding(10.dp),
)
Text(
stringResource(id = R.string.export_amnezia),
modifier = Modifier.padding(10.dp),
)
}
HorizontalDivider()
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
showExportSheet = false
viewModel.exportAllConfigs(context, ConfigType.WG)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.FolderZip,
contentDescription = stringResource(id = R.string.export_wireguard),
modifier = Modifier.padding(10.dp),
)
Text(
stringResource(id = R.string.export_wireguard),
modifier = Modifier.padding(10.dp),
)
}
}
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
@@ -5,11 +5,13 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@@ -21,16 +23,23 @@ constructor(
private val fileUtils: FileUtils,
) : ViewModel() {
fun exportAllConfigs(context: Context) = viewModelScope.launch {
fun exportAllConfigs(context: Context, configType: ConfigType) = viewModelScope.launch {
runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels)
val amFiles = fileUtils.createAmFiles(tunnels)
val allFiles = wgFiles + amFiles
fileUtils.zipAll(shareFile, allFiles)
val (files, shareFileName) = when (configType) {
ConfigType.AMNEZIA -> {
Pair(fileUtils.createAmFiles(tunnels), "am-export_${Instant.now().epochSecond}.zip")
}
ConfigType.WG -> {
Pair(fileUtils.createWgFiles(tunnels), "wg-export_${Instant.now().epochSecond}.zip")
}
}
val shareFile = fileUtils.createNewShareFile(shareFileName)
fileUtils.zipAll(shareFile, files)
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile)
context.launchShareFile(uri)
}.onFailure {
Timber.e(it)
}
}
}
+2
View File
@@ -213,4 +213,6 @@
<string name="unauthorized">Failed to start tunnel, unauthorized.</string>
<string name="tunne_start_failed_title">Tunnel failure</string>
<string name="multiple">Multiple</string>
<string name="export_amnezia">Export as Amnezia</string>
<string name="export_wireguard">Export as WireGuard</string>
</resources>
+7 -3
View File
@@ -10,7 +10,7 @@ datastorePreferences = "1.1.2"
desugar_jdk_libs = "2.1.4"
espressoCore = "3.6.1"
hiltAndroid = "2.55"
hiltNavigationCompose = "1.2.0"
hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.8.0"
lifecycle-runtime-compose = "2.8.7"
@@ -20,11 +20,12 @@ pinLockCompose = "1.0.4"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.2"
androidGradlePlugin = "8.10.0-alpha05"
androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10"
ksp = "2.1.10-1.0.30"
composeBom = "2025.02.00"
compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0"
@@ -45,6 +46,7 @@ amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltCompiler" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
@@ -62,15 +64,17 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compos
#hilt
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtxVersion" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompiler" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" }