changed vpn service to ServiceViewModel

This commit is contained in:
asxalex 2025-07-09 16:43:59 +08:00
parent e0061c1277
commit 91042c083f
13 changed files with 1235 additions and 54 deletions

View File

@ -3,15 +3,19 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
@ -27,21 +31,27 @@
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true"> <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
</profile> </profile>
</component> </component>

View File

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
id("com.google.protobuf") version "0.9.4" id("com.google.protobuf") version "0.9.4"
id("com.google.devtools.ksp") version "2.0.0-1.0.22"
} }
android { android {
@ -85,6 +86,10 @@ dependencies {
implementation("androidx.navigation:navigation-compose:2.9.0") implementation("androidx.navigation:navigation-compose:2.9.0")
implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-compose:2.6.0")
implementation("androidx.room:room-runtime:2.7.2")
ksp("androidx.room:room-compiler:2.7.2")
implementation("androidx.room:room-ktx:2.7.2")
// implementation(libs.bcprov.jdk18on) // implementation(libs.bcprov.jdk18on)
implementation(libs.bcpkix.jdk18on) implementation(libs.bcpkix.jdk18on)
// implementation(files("libs/org.asxalex.sdlan/sdlan-1.0-SNAPSHOT.jar")) // implementation(files("libs/org.asxalex.sdlan/sdlan-1.0-SNAPSHOT.jar"))

View File

@ -1,5 +1,6 @@
package com.jihe.punchnet package com.jihe.punchnet
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
@ -7,9 +8,11 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -22,46 +25,54 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.rememberNavController
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
import com.jihe.punchnet.screen.MainApp
import com.jihe.punchnet.ui.theme.PunchnetTheme import com.jihe.punchnet.ui.theme.PunchnetTheme
import kotlinx.coroutines.flow.onEach
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"
private val vpnPermissionLauncher = registerForActivityResult( private val viewModel: RouteViewModel by viewModels {
ActivityResultContracts.StartActivityForResult() object: ViewModelProvider.Factory {
) { result -> @Suppress("UNCHECKED_CAST")
when(result.resultCode) { override fun<T: ViewModel> create(modelClass: Class<T>): T {
RESULT_OK -> { return RouteViewModel(application) as T
// granted permission, start service
}
RESULT_CANCELED -> {
Toast.makeText(this, "VPN permission denied", Toast.LENGTH_SHORT).show()
} }
} }
} }
private val serviceModel: ServiceViewModel by viewModels {
object: ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun<T: ViewModel> create(modelClass: Class<T>): T {
return ServiceViewModel(application) as T
}
}
}
/*
fun prepareAndStartVPN() { fun prepareAndStartVPN() {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent != null) { if (intent != null) {
vpnPermissionLauncher.launch(intent) vpnPermissionLauncher.launch(intent)
} else { } else {
startVpnService() startVpnService(this)
} }
} }
*/
fun stopVpnService() { /*
Log.d("STOP PUNCHNET", "stopping PUNCHNET") private fun startVpnService(context: Context) {
startService(Intent(this@MainActivity, PunchnetService::class.java).also { it.action=PunchnetService.ACTION_DISCONNECT }) val intent = Intent(context, PunchnetService::class.java)
// stopService(Intent(this, PunchnetService::class.java))
Log.d("STOPPED PUNCHNET", "stopping PUNCHNET")
// Toast.makeText(this, "VPN service stopped", Toast.LENGTH_SHORT).show()
}
private fun startVpnService() {
val intent = Intent(this, PunchnetService::class.java)
intent.putExtra("argument", PunchnetServiceArgument( intent.putExtra("argument", PunchnetServiceArgument(
"", "",
arrayOf( arrayOf(
@ -77,14 +88,19 @@ class MainActivity : ComponentActivity() {
} }
Toast.makeText(this, "VPN service started", Toast.LENGTH_SHORT).show() Toast.makeText(this, "VPN service started", Toast.LENGTH_SHORT).show()
} }
*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
Log.d("DIR", "filesdir = ${this.filesDir}") Log.d("DIR", "filesdir = ${this.filesDir}")
setContent { setContent {
PunchnetTheme { PunchnetTheme {
MainApp(serviceModel, viewModel, rememberNavController())
/*
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
StartStop( StartStop(
R.string.start_vpn, R.string.start_vpn,
@ -93,11 +109,13 @@ class MainActivity : ComponentActivity() {
prepareAndStartVPN() prepareAndStartVPN()
}, },
onStop = { onStop = {
stopVpnService() // stopVpnService()
}, },
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) )
} }
*/
} }
} }
} }

View File

@ -0,0 +1,109 @@
package com.jihe.punchnet.data
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@Entity(tableName = "routes")
data class RouteItem (
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "net_ip")
val net_ip: Int,
// mask int, Intof(255,255,255,0
@ColumnInfo(name = "mask_ip")
val mask_ip: Int,
@ColumnInfo(name = "gateway")
val gateway: Int,
)
@Dao
interface RouteDAO {
@Insert
suspend fun insert(item: RouteItem): Long
@Update
suspend fun update(item: RouteItem)
@Query("DELETE FROM routes WHERE id = :id")
suspend fun deleteById(id: Long)
@Query("SELECT * FROM routes")
fun getAll(): Flow<List<RouteItem>>
}
@Database(entities = [RouteItem::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun routeDao(): RouteDAO
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
class RouteRepository(private val routeDao: RouteDAO) {
val all_routes: Flow<List<RouteItem>> = routeDao.getAll()
suspend fun insert(item: RouteItem) {
routeDao.insert(item)
}
suspend fun update(item: RouteItem) {
routeDao.update(item)
}
suspend fun deleteById(id: Long) {
routeDao.deleteById(id)
}
}
class RouteViewModel(application: Application): AndroidViewModel(application) {
private val repository: RouteRepository
init {
val routeDao = AppDatabase.getDatabase(application).routeDao()
repository = RouteRepository(routeDao)
}
val allRoutes: Flow<List<RouteItem>> = repository.all_routes
fun insert(item: RouteItem) = viewModelScope.launch {
repository.insert(item)
}
fun update(item: RouteItem) = viewModelScope.launch {
repository.update(item)
}
fun deleteById(id: Long) = viewModelScope.launch {
repository.deleteById(id)
}
}

View File

@ -0,0 +1,75 @@
package com.jihe.punchnet.data
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.ContextCompat.startForegroundService
import androidx.lifecycle.AndroidViewModel
import com.jihe.punchnet.PunchnetService
import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.RouteInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ServiceViewModel(application: Application): AndroidViewModel(application) {
private val _isRunning = mutableStateOf(false)
val isRunning: State<Boolean> = _isRunning
private val _hasVpnPermission = mutableStateOf(false)
val hasVpnPermission: State<Boolean> = _hasVpnPermission
fun setVPNPermission(isGranted: Boolean) {
_hasVpnPermission.value = isGranted
}
fun startService(context: Context, noPermissionCallback: (Intent)->Unit) {
val intent = VpnService.prepare(context)
if (intent != null) {
noPermissionCallback(intent)
} else {
startVpnService(context)
_isRunning.value = true
}
}
private fun startVpnService(context: Context) {
val intent = Intent(context, PunchnetService::class.java)
intent.putExtra("argument", PunchnetServiceArgument(
"",
arrayOf(
RouteInfo(
"192.168.80.0/24", "10.211.188.2"
)
)
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Toast.makeText(context, "VPN service started", Toast.LENGTH_SHORT).show()
}
fun stopVpnService(context: Context) {
Log.d("STOP PUNCHNET", "stopping PUNCHNET")
context.startService(Intent(context, PunchnetService::class.java).also { it.action=
PunchnetService.ACTION_DISCONNECT })
// stopService(Intent(this, PunchnetService::class.java))
Log.d("STOPPED PUNCHNET", "stopping PUNCHNET")
_isRunning.value = false
// Toast.makeText(this, "VPN service stopped", Toast.LENGTH_SHORT).show()
}
}

View File

@ -0,0 +1,66 @@
package com.jihe.punchnet.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.jihe.punchnet.R
import com.jihe.punchnet.RouteInfo
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
@Composable
fun AppNavHost2(
serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel = viewModel(),
navController: NavHostController,
paddingValues: PaddingValues
) {
NavHost(
navController = navController,
startDestination = Screen.MainScreen.route,
modifier = Modifier.padding(paddingValues)
) {
composable(Screen.MainScreen.route) {
HomeScreen(serviceViewModel, routeViewModel)
}
composable(Screen.RouteScreen.route) {
CustomHeaderScreen(
"光强计"
) {
// LightScreen()
}
}
composable(Screen.ProfileScreen.route) {
// Profile(dbdao, navController)
}
}
}

View File

@ -0,0 +1,66 @@
package com.jihe.punchnet.screen
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun AppBottomNavigation(
navController: NavHostController
) {
// val currentRoute = navController.currentBackStackEntry?.destination?.route
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute by remember {
derivedStateOf {
navBackStackEntry?.destination?.route
}
}
NavigationBar {
bottomNavItems.forEach { screen ->
NavigationBarItem(
icon = {
Icon(
imageVector = when (screen) {
Screen.MainScreen -> Icons.Default.Home
Screen.RouteScreen -> Icons.Default.Menu
// Screen.Search -> Icons.Default.Search
Screen.ProfileScreen -> Icons.Default.Person
},
contentDescription = screen.route
)
},
label = {
Text(
text = screen.route.replaceFirstChar { it.uppercase() },
)
},
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route) {
launchSingleTop = true
restoreState= true
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
}
println("navigate to ${screen.route}")
}
)
}
}
}

View File

@ -0,0 +1,164 @@
package com.jihe.punchnet.screen
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.jihe.punchnet.PunchnetService
import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.R
import com.jihe.punchnet.RouteInfo
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
@Composable
fun HomeScreen(
serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel,
// started: MutableState<Boolean>,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val vpnLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {result ->
when(result.resultCode) {
RESULT_OK -> {
serviceViewModel.setVPNPermission(true)
}
RESULT_CANCELED -> {
serviceViewModel.setVPNPermission(false)
Toast.makeText(context, "vpn permission denied", Toast.LENGTH_SHORT).show()
}
}
}
CustomHeaderScreen(
"",
onBack = null,
onMenu = {
Icon(
Icons.Default.Settings,
contentDescription = "settings",
modifier = Modifier.fillMaxHeight()
)
// NavListMenu(navController, dbdao)
/*
navController.navigate(
Screen.ActionDetail.route.replace(
"{action_name}",
""
)
)
*/
}
) {
Column(
modifier = Modifier.fillMaxWidth()
.padding(top=50.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.punchnet_log),
contentDescription = "logo",
modifier = Modifier.size(150.dp)
)
Text(
"Connecting the Infinite",
fontSize = 30.sp,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top=24.dp)
)
Text(
"Welcome to PunchNet",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top=8.dp)
)
Button(
onClick = {
if (serviceViewModel.isRunning.value) {
// if is running, should stop service
serviceViewModel.stopVpnService(context)
} else {
serviceViewModel.startService(
context,
{intent->
vpnLauncher.launch(intent)
},
)
}
// started.value = !started.value
},
shape = RoundedCornerShape(10.dp),
modifier = Modifier.padding(top=48.dp)
.width(120.dp)
.height(40.dp)
) {
Text(
if (serviceViewModel.isRunning.value) {
"停止"
} else {
"启动"
}
)
}
}
// BasicCardList(dbdao = dbdao, navController)
}
}
private fun startVpnService(context: Context) {
val intent = Intent(context, PunchnetService::class.java)
intent.putExtra("argument", PunchnetServiceArgument(
"",
arrayOf(
RouteInfo(
"192.168.80.0/24", "10.211.188.2"
)
)
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Toast.makeText(context, "VPN service started", Toast.LENGTH_SHORT).show()
}

View File

@ -1,7 +1,6 @@
package com.jihe.punchnet.screen package com.jihe.punchnet.screen
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -12,22 +11,51 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
sealed class Screen(val route: String) {
object MainScreen: Screen("main");
object RouteScreen: Screen("routes")
object ProfileScreen: Screen("profile")
}
val bottomNavItems = listOf(
Screen.MainScreen,
Screen.RouteScreen,
Screen.ProfileScreen,
)
@Composable @Composable
fun MainApp( fun MainApp(
serviceViewModel: ServiceViewModel = viewModel(),
routeModel: RouteViewModel = viewModel(),
navController: NavHostController, navController: NavHostController,
) { ) {
Scaffold(
bottomBar = {
AppBottomNavigation(navController = navController)
}
){ paddingValues ->
/*
Image(
painter = painterResource(R.drawable.punchnet_log),
contentDescription = null,
modifier = Modifier.padding(paddingValues).size(200.dp)
)
*/
AppNavHost2(serviceViewModel, routeModel, navController, paddingValues)
}
} }
@Composable @Composable
@ -38,13 +66,13 @@ fun CustomHeaderScreen(
body: @Composable () -> Unit, body: @Composable () -> Unit,
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) { ) {
Box ( Box (
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp) .height(56.dp)
.background(MaterialTheme.colorScheme.surfaceContainer), .background(MaterialTheme.colorScheme.background),
//.padding(horizontal = 16.dp), //.padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
@ -78,23 +106,9 @@ fun CustomHeaderScreen(
) { ) {
if (onMenu != null) { if (onMenu != null) {
onMenu() onMenu()
/*
Icon(
modifier = Modifier
.clickable {
onMenu()
}
.fillMaxSize(),
imageVector = Icons.Filled.Menu,
contentDescription = "setting",
// tint = MaterialTheme.colorScheme.onPrimary
)
*/
} }
} }
} }
} }

View File

@ -11,6 +11,10 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
const val BroadcastIP = 0xFFFFFFFF
@OptIn(ExperimentalUnsignedTypes::class)
val BroadcastMac = ubyteArrayOf(0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu).toByteArray()
class ARPTable( class ARPTable(
) { ) {
val routeTable: RouteTable = RouteTable() val routeTable: RouteTable = RouteTable()
@ -36,15 +40,17 @@ class ARPTable(
} }
} }
fun getMacFromIP(ip: Int): ByteArray? { fun getMacFromIP(ip: Int): Pair<ByteArray?, Int> {
val value = content.get(ip) val gw = routeTable.getGateway(ip)
if (value == null) { if (gw == null) {
println("content table size is: ${content.size}") // not found in route table, just use the ip
for (k in content.keys()) { val value = content.get(ip)
println("content has key: ${ipToString(k)}") return Pair(value?.mac, ip)
} } else {
// gw not null, try find the gw's mac
val value = content.get(gw)
return Pair(value?.mac, gw)
} }
return value?.mac
} }
fun addToARPTable(ip: Int, mac: ByteArray) { fun addToARPTable(ip: Int, mac: ByteArray) {

View File

@ -175,13 +175,13 @@ interface IfaceTun: Iface {
return return
} }
val arpinfo = arpTable.getMacFromIP(dstip) val (arpinfo, gwip) = arpTable.getMacFromIP(dstip)
if (arpinfo == null) { if (arpinfo == null) {
println("arp info is null") println("arp info is null")
// arp not found // arp not found
arpWaitList.addToWaitList(dstip, data) arpWaitList.addToWaitList(gwip, data)
TerminalLogger.debugf { "added to wait list" } TerminalLogger.debugf { "added to wait list" }
sendArpRequest(node, dstip) sendArpRequest(node, gwip)
TerminalLogger.debugf { "sent arp request" } TerminalLogger.debugf { "sent arp request" }
} else { } else {
println("mac is ${macToString(arpinfo.toByteString())}") println("mac is ${macToString(arpinfo.toByteString())}")

View File

@ -90,7 +90,7 @@ class RouteTable() {
} }
} }
fun getGeteway(ip: Int): Int? { fun getGateway(ip: Int): Int? {
try { try {
lock.readLock().lock() lock.readLock().lock()
for (item in routeInfo) { for (item in routeInfo) {

File diff suppressed because one or more lines are too long