diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index 359aa0f..a9f3e4e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -55,6 +55,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen @@ -209,6 +210,9 @@ class MainActivity : AppCompatActivity() { appViewModel = viewModel, ) } + composable { + ScannerScreen() + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index f570341..8a3af84 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -20,6 +20,9 @@ sealed class Route { @Serializable data object Lock : Route() + @Serializable + data object Scanner : Route() + @Serializable data class Config( val id: Int, 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 e5ae7e1..0464605 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 @@ -36,8 +36,6 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.journeyapps.barcodescanner.ScanContract -import com.journeyapps.barcodescanner.ScanOptions import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.ui.AppUiState @@ -105,15 +103,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, viewModel.onTunnelFileSelected(data, context) }) - val scanLauncher = - rememberLauncherForActivityResult( - contract = ScanContract(), - onResult = { - if (it.contents != null) { - viewModel.onTunnelQrResult(it.contents) - } - }, - ) + val requestPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (!isGranted) return@rememberLauncherForActivityResult snackbar.showMessage("Camera permission required") + navController.navigate(Route.Scanner) + } VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) @@ -142,17 +137,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, } } - fun launchQrScanner() { - val scanOptions = ScanOptions() - scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) - scanOptions.setOrientationLocked(true) - scanOptions.setPrompt( - context.getString(R.string.scanning_qr), - ) - scanOptions.setBeepEnabled(false) - scanLauncher.launch(scanOptions) - } - Scaffold( modifier = Modifier.pointerInput(Unit) { @@ -181,7 +165,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, showBottomSheet, onDismiss = { showBottomSheet = false }, onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) }, - onQrClick = { launchQrScanner() }, + onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, onManualImportClick = { navController.navigate( Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID), 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 8792d60..728354e 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 @@ -100,21 +100,6 @@ constructor( return defaultName } - fun onTunnelQrResult(result: String) = viewModelScope.launch(ioDispatcher) { - kotlin.runCatching { - val amConfig = TunnelConfig.configFromAmQuick(result) - val amQuick = amConfig.toAwgQuickString(true) - val wgQuick = amConfig.toWgQuickString() - - val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result)) - val tunnelConfig = TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick) - saveTunnel(tunnelConfig) - }.onFailure { - Timber.e(it) - SnackbarController.showMessage(StringValue.StringResource(R.string.error_invalid_code)) - } - } - private suspend fun makeTunnelNameUnique(name: String): String { return withContext(ioDispatcher) { val tunnels = appDataRepository.tunnels.getAll() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerScreen.kt new file mode 100644 index 0000000..c6a9dfd --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerScreen.kt @@ -0,0 +1,46 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.scanner + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.journeyapps.barcodescanner.CompoundBarcodeView +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) { + val context = LocalContext.current + val navController = LocalNavController.current + + val success = viewModel.success.collectAsStateWithLifecycle(null) + + LaunchedEffect(success.value) { + if (success.value != null) navController.popBackStack() + } + + val barcodeView = remember { + CompoundBarcodeView(context).apply { + this.initializeFromIntent((context as Activity).intent) + this.setStatusText("") + this.decodeSingle { result -> + result.text?.let { barCodeOrQr -> + viewModel.onTunnelQrResult(barCodeOrQr) + } + } + } + } + AndroidView(factory = { barcodeView }) + DisposableEffect(Unit) { + barcodeView.resume() + onDispose { + barcodeView.pause() + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerViewModel.kt new file mode 100644 index 0000000..21b08d0 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/scanner/ScannerViewModel.kt @@ -0,0 +1,69 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.scanner + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.IoDispatcher +import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController +import com.zaneschepke.wireguardautotunnel.util.NumberUtils +import com.zaneschepke.wireguardautotunnel.util.StringValue +import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ScannerViewModel @Inject +constructor( + private val appDataRepository: AppDataRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) : ViewModel() { + + private val _success = MutableSharedFlow() + val success = _success.asSharedFlow() + + private suspend fun makeTunnelNameUnique(name: String): String { + return withContext(ioDispatcher) { + val tunnels = appDataRepository.tunnels.getAll() + var tunnelName = name + var num = 1 + while (tunnels.any { it.name == tunnelName }) { + tunnelName = "$name($num)" + num++ + } + tunnelName + } + } + + fun onTunnelQrResult(result: String) = viewModelScope.launch(ioDispatcher) { + kotlin.runCatching { + val amConfig = TunnelConfig.configFromAmQuick(result) + val amQuick = amConfig.toAwgQuickString(true) + val wgQuick = amConfig.toWgQuickString() + val tunnelName = makeTunnelNameUnique(generateQrCodeDefaultName(result)) + val tunnelConfig = TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick) + appDataRepository.tunnels.save(tunnelConfig) + _success.emit(true) + }.onFailure { + _success.emit(false) + Timber.e(it) + SnackbarController.showMessage(StringValue.StringResource(R.string.error_invalid_code)) + } + } + + private fun generateQrCodeDefaultName(config: String): String { + return try { + TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host + } catch (e: Exception) { + Timber.e(e) + NumberUtils.generateRandomTunnelName() + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt new file mode 100644 index 0000000..646c728 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt @@ -0,0 +1,10 @@ +package com.zaneschepke.wireguardautotunnel.util.extensions + +import androidx.navigation.NavController +import com.zaneschepke.wireguardautotunnel.ui.Route + +fun NavController.navigateAndForget(route: Route) { + navigate(route) { + popUpTo(0) + } +}