parent
2f53a8f3b4
commit
49bf7fa8b9
|
@ -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
|
||||
|
|
|
@ -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,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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue