From 49bf7fa8b91c2259e95e3bd7fedfcbe2390783dc Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sun, 2 Jul 2023 22:41:35 -0400 Subject: [PATCH] feat: add scan qr code option for importing tunnel configs Closes #1 --- app/build.gradle.kts | 13 +++-- app/src/main/AndroidManifest.xml | 4 ++ .../module/ScannerModule.kt | 41 ++++++++++++++ .../module/ServiceModule.kt | 3 ++ .../service/barcode/CodeScanner.kt | 7 +++ .../service/barcode/QRScanner.kt | 22 ++++++++ .../service/tunnel/WireGuardTunnel.kt | 6 +-- .../ui/screens/main/MainScreen.kt | 53 ++++++++++++++++++- .../ui/screens/main/MainViewModel.kt | 21 ++++++-- build.gradle.kts | 1 + 10 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ScannerModule.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/CodeScanner.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/QRScanner.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5a1f4bc..110dbc3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b8ef75..b38fe4a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + @@ -59,5 +60,8 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ScannerModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ScannerModule.kt new file mode 100644 index 0000000..dc506ac --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ScannerModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt index 59f6644..c6d3b4c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/CodeScanner.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/CodeScanner.kt new file mode 100644 index 0000000..2c0a5ad --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/CodeScanner.kt @@ -0,0 +1,7 @@ +package com.zaneschepke.wireguardautotunnel.service.barcode + +import kotlinx.coroutines.flow.Flow + +interface CodeScanner { + fun scan() : Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/QRScanner.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/QRScanner.kt new file mode 100644 index 0000000..ab71f7c --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/barcode/QRScanner.kt @@ -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 { + return callbackFlow { + gmsBarcodeScanner.startScan().addOnSuccessListener { + trySend(it.rawValue) + }.addOnFailureListener { + Timber.e(it.message) + } + awaitClose { + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 56b8a79..def01b1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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 } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 415987e..b45b62a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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 } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 3fa0e45..9d652f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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, private val settingsRepo : Repository, - 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) { diff --git a/build.gradle.kts b/build.gradle.kts index 84e9717..3ecb7a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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")