feat: add scan qr code option for importing tunnel configs

Closes #1
This commit is contained in:
Zane Schepke 2023-07-02 22:41:35 -04:00
parent 2f53a8f3b4
commit 49bf7fa8b9
10 changed files with 158 additions and 13 deletions

View File

@ -16,8 +16,8 @@ android {
compileSdk = 33
val versionMajor = 1
val versionMinor = 1
val versionPatch = 6
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

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>

View File

@ -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)
}
}

View File

@ -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)

View File

@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}

View File

@ -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 {
}
}
}
}

View File

@ -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
}
}

View File

@ -4,28 +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.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
@ -33,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
@ -45,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
@ -68,6 +81,8 @@ 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) }
@ -109,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),
@ -133,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,
@ -181,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
}

View File

@ -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) {

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")