Compare commits

..

2 Commits

Author SHA1 Message Date
Zane Schepke 49bf7fa8b9 feat: add scan qr code option for importing tunnel configs
Closes #1
2023-07-02 22:45:36 -04:00
Zane Schepke 2f53a8f3b4 Update VPN connection required message to add always-on VPN disclaimer 2023-07-02 11:59:24 -04:00
11 changed files with 159 additions and 16 deletions
+9 -4
View File
@@ -16,8 +16,8 @@ android {
compileSdk = 33
val versionMajor = 1
val versionMinor = 1
val versionPatch = 5
val versionMinor = 2
val versionPatch = 0
val versionBuild = 0
defaultConfig {
@@ -71,7 +71,7 @@ dependencies {
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
@@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.5.3")
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
@@ -120,6 +120,11 @@ dependencies {
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
}
kapt {
correctErrorTypes = true
+4
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@@ -59,5 +60,8 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
</application>
</manifest>
@@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
class ScannerModule {
@ViewModelScoped
@Provides
fun provideBarCodeOptions() : GmsBarcodeScannerOptions {
return GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
}
@ViewModelScoped
@Provides
fun provideBarCodeScanner(@ApplicationContext context: Context, options: GmsBarcodeScannerOptions) : GmsBarcodeScanner {
return GmsBarcodeScanning.getClient(context, options)
}
@ViewModelScoped
@Provides
fun provideQRScanner(gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
return QRScanner(gmsBarcodeScanner)
}
}
@@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -10,6 +12,7 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}
@@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
override fun scan(): Flow<String?> {
return callbackFlow {
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
Timber.e(it.message)
}
awaitClose {
}
}
}
}
@@ -24,7 +24,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe
override val state get() = _state.asSharedFlow()
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
try {
return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
@@ -33,10 +33,10 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe
val state = backend.setState(
this, Tunnel.State.UP, config)
_state.emit(state)
return state;
state;
} catch (e : Exception) {
Timber.e("Failed to start tunnel with error: ${e.message}")
return Tunnel.State.DOWN
Tunnel.State.DOWN
}
}
@@ -4,29 +4,38 @@ import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
@@ -34,6 +43,8 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -46,6 +57,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
@@ -69,11 +81,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
val context = LocalContext.current
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val viewState = viewModel.viewState.collectAsStateWithLifecycle()
var showAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val settings by viewModel.settings.collectAsStateWithLifecycle()
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
@@ -111,7 +124,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
FloatingActionButton(
modifier = Modifier.padding(bottom = 90.dp),
onClick = {
pickFileLauncher.launch("*/*")
showBottomSheet = true
},
containerColor = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp),
@@ -135,6 +148,36 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
}
}
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Row(
modifier = Modifier.fillMaxWidth().clickable {
showBottomSheet = false
pickFileLauncher.launch("*/*")
}.padding(10.dp)
) {
Icon(Icons.Filled.FileOpen, contentDescription = "File Open", modifier = Modifier.padding(10.dp))
Text("Add tunnel from files", modifier = Modifier.padding(10.dp))
}
Divider()
Row(modifier = Modifier.fillMaxWidth().clickable {
scope.launch {
showBottomSheet = false
viewModel.onTunnelQRSelected()
}
}.padding(10.dp)
) {
Icon(Icons.Filled.QrCode, contentDescription = "QR Scan", modifier = Modifier.padding(10.dp))
Text("Add tunnel from QR code", modifier = Modifier.padding(10.dp))
}
}
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
@@ -183,7 +226,11 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
}, confirmButton = {
Button(onClick = {
if (tunnels.any { it.name == selectedTunnel?.name }) {
Toast.makeText(context, context.resources.getString(R.string.tunnel_exists), Toast.LENGTH_LONG)
Toast.makeText(
context,
context.resources.getString(R.string.tunnel_exists),
Toast.LENGTH_LONG
)
.show()
return@Button
}
@@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
@@ -32,7 +33,8 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>,
private val settingsRepo : Repository<Settings>,
private val vpnService: VpnService
private val vpnService: VpnService,
private val codeScanner: CodeScanner
) : ViewModel() {
private val _viewState = MutableStateFlow(ViewState())
@@ -43,7 +45,9 @@ class MainViewModel @Inject constructor(private val application : Application,
private val _settings = MutableStateFlow(Settings())
val settings get() = _settings.asStateFlow()
private val defaultConfigName = "tunnel${(Math.random() * 1000).toInt()}"
private val defaultConfigName = {
"tunnel${(Math.random() * 100000).toInt()}"
}
init {
@@ -111,6 +115,15 @@ class MainViewModel @Inject constructor(private val application : Application,
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java)
}
suspend fun onTunnelQRSelected() {
codeScanner.scan().collect {
Timber.d(it)
if(!it.isNullOrEmpty()) {
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it))
}
}
}
fun onTunnelFileSelected(uri : Uri) {
val fileName = getFileName(application.applicationContext, uri)
val extension = getFileExtensionFromFileName(fileName)
@@ -135,14 +148,14 @@ class MainViewModel @Inject constructor(private val application : Application,
private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor ?: return defaultConfigName
cursor ?: return defaultConfigName()
cursor.use {
if(cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
return defaultConfigName
return defaultConfigName()
}
suspend fun showSnackBarMessage(message : String) {
+1 -1
View File
@@ -17,7 +17,7 @@
<string name="watcher_notification_text">Monitoring network state changes</string>
<string name="tunnel_start_title">VPN Connected</string>
<string name="tunnel_start_text">Connected to tunnel -</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly.</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
<string name="open_settings">Open Settings</string>
<string name="add_trusted_ssid">Add Trusted SSID</string>
+1
View File
@@ -4,6 +4,7 @@ buildscript {
val objectBoxVersion by extra("3.5.1")
val hiltVersion by extra("2.44")
val accompanistVersion by extra("0.31.2-alpha")
val cameraVersion by extra("1.3.0-beta01")
dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")