rearrange Ui, prep for ssl support, removed iptables and resorting to use syscalls to allow ports bwlo 1024
This commit is contained in:
@@ -98,4 +98,52 @@ object RootManager {
|
||||
}
|
||||
} ?: Pair(false, "Timeout")
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow binding to privileged ports by lowering the unprivileged port start.
|
||||
* This uses sysctl to set net.ipv4.ip_unprivileged_port_start to the specified port.
|
||||
*/
|
||||
suspend fun allowPrivilegedPort(port: Int): Boolean = withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(5000L) {
|
||||
try {
|
||||
val command = "sysctl -w net.ipv4.ip_unprivileged_port_start=$port"
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
val completed = process.waitFor(4, TimeUnit.SECONDS)
|
||||
if (completed) {
|
||||
val success = process.exitValue() == 0
|
||||
if (success) {
|
||||
android.util.Log.i("RootManager", "Allowed binding to ports >= $port")
|
||||
}
|
||||
success
|
||||
} else {
|
||||
process.destroyForcibly()
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("RootManager", "Failed to allow privileged port", e)
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the unprivileged port start back to default (1024).
|
||||
*/
|
||||
suspend fun resetPrivilegedPorts(): Boolean = withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(5000L) {
|
||||
try {
|
||||
val command = "sysctl -w net.ipv4.ip_unprivileged_port_start=1024"
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
val completed = process.waitFor(4, TimeUnit.SECONDS)
|
||||
if (completed) {
|
||||
process.exitValue() == 0
|
||||
} else {
|
||||
process.destroyForcibly()
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ class TextpipeService : Service() {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val PREFS_NAME = "textpipe_settings"
|
||||
private const val KEY_PORT = "server_port"
|
||||
private const val KEY_REDIRECT_PORT = "redirect_port"
|
||||
private const val KEY_SSL_ENABLED = "ssl_enabled"
|
||||
private const val KEY_SSL_CERT_PATH = "ssl_cert_path"
|
||||
private const val KEY_SSL_KEY_PATH = "ssl_key_path"
|
||||
private const val DEFAULT_PORT = 8080
|
||||
private const val ACTION_STOP = "sh.sar.textpipe.STOP"
|
||||
|
||||
@@ -70,14 +72,34 @@ class TextpipeService : Service() {
|
||||
prefs.edit().putInt(KEY_PORT, port).apply()
|
||||
}
|
||||
|
||||
fun getRedirectPort(context: Context): Int {
|
||||
fun isSslEnabled(context: Context): Boolean {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getInt(KEY_REDIRECT_PORT, 0)
|
||||
return prefs.getBoolean(KEY_SSL_ENABLED, false)
|
||||
}
|
||||
|
||||
fun setRedirectPort(context: Context, port: Int) {
|
||||
fun setSslEnabled(context: Context, enabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putInt(KEY_REDIRECT_PORT, port).apply()
|
||||
prefs.edit().putBoolean(KEY_SSL_ENABLED, enabled).apply()
|
||||
}
|
||||
|
||||
fun getSslCertPath(context: Context): String {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getString(KEY_SSL_CERT_PATH, "") ?: ""
|
||||
}
|
||||
|
||||
fun setSslCertPath(context: Context, path: String) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(KEY_SSL_CERT_PATH, path).apply()
|
||||
}
|
||||
|
||||
fun getSslKeyPath(context: Context): String {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getString(KEY_SSL_KEY_PATH, "") ?: ""
|
||||
}
|
||||
|
||||
fun setSslKeyPath(context: Context, path: String) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(KEY_SSL_KEY_PATH, path).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +108,7 @@ class TextpipeService : Service() {
|
||||
private var textpipeServer: TextpipeServer? = null
|
||||
private lateinit var smsRepository: SmsRepository
|
||||
private lateinit var smsSender: SmsSender
|
||||
|
||||
private var redirectPort: Int = 0
|
||||
private var usedPrivilegedPort = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -125,14 +146,18 @@ class TextpipeService : Service() {
|
||||
_serverPort.value = port
|
||||
|
||||
serviceScope.launch(Dispatchers.IO) {
|
||||
// Setup port redirect if configured
|
||||
redirectPort = getRedirectPort(this@TextpipeService)
|
||||
if (redirectPort > 0 && redirectPort != port) {
|
||||
val success = RootManager.setupPortRedirect(redirectPort, port)
|
||||
if (!success) {
|
||||
Log.w(TAG, "Failed to setup port redirect from $redirectPort to $port")
|
||||
setRedirectPort(this@TextpipeService, 0)
|
||||
redirectPort = 0
|
||||
// If port < 1024, we need root to allow binding
|
||||
if (port < 1024) {
|
||||
Log.i(TAG, "Port $port requires root access")
|
||||
val allowed = RootManager.allowPrivilegedPort(port)
|
||||
if (allowed) {
|
||||
usedPrivilegedPort = true
|
||||
Log.i(TAG, "Privileged port access granted for port $port")
|
||||
} else {
|
||||
updateNotification("Error: Root required for port $port")
|
||||
showToast("Root access required for port $port")
|
||||
Log.e(TAG, "Failed to get root access for port $port")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +165,8 @@ class TextpipeService : Service() {
|
||||
when (val result = textpipeServer?.start(port)) {
|
||||
is ServerStartResult.Success -> {
|
||||
_isRunning.value = true
|
||||
val displayPort = if (redirectPort > 0) redirectPort else port
|
||||
updateNotification("Running on port $displayPort")
|
||||
showToast("Server started on port $displayPort")
|
||||
updateNotification("Running on port $port")
|
||||
showToast("Server started on port $port")
|
||||
Log.i(TAG, "Server started successfully on port $port")
|
||||
}
|
||||
is ServerStartResult.Error -> {
|
||||
@@ -164,12 +188,12 @@ class TextpipeService : Service() {
|
||||
override fun onDestroy() {
|
||||
Log.i(TAG, "Service onDestroy")
|
||||
|
||||
// Clear port redirect asynchronously (fire and forget to avoid ANR)
|
||||
if (redirectPort > 0) {
|
||||
val port = textpipeServer?.getPort() ?: 8080
|
||||
// Reset privileged port setting if we used it
|
||||
if (usedPrivilegedPort) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
RootManager.clearPortRedirect(redirectPort, port)
|
||||
RootManager.resetPrivilegedPorts()
|
||||
}
|
||||
usedPrivilegedPort = false
|
||||
}
|
||||
|
||||
textpipeServer?.stop()
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package sh.sar.textpipe.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -47,6 +50,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import sh.sar.textpipe.sim.SimSlotInfo
|
||||
|
||||
@@ -57,9 +61,11 @@ fun MainScreen(viewModel: MainViewModel) {
|
||||
val simSlots by viewModel.simSlots.collectAsState()
|
||||
val isRootAvailable by viewModel.isRootAvailable.collectAsState()
|
||||
val portInput by viewModel.portInput.collectAsState()
|
||||
val redirectPortInput by viewModel.redirectPortInput.collectAsState()
|
||||
val autoStartEnabled by viewModel.autoStartEnabled.collectAsState()
|
||||
val isBatteryOptimized by viewModel.isBatteryOptimized.collectAsState()
|
||||
val sslEnabled by viewModel.sslEnabled.collectAsState()
|
||||
val sslCertPath by viewModel.sslCertPath.collectAsState()
|
||||
val sslKeyPath by viewModel.sslKeyPath.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
@@ -116,9 +122,9 @@ fun MainScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
// Server Configuration
|
||||
Text(
|
||||
text = "Settings",
|
||||
text = "Server Configuration",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -126,14 +132,31 @@ fun MainScreen(viewModel: MainViewModel) {
|
||||
// Port Configuration Card
|
||||
PortConfigCard(
|
||||
portInput = portInput,
|
||||
redirectPortInput = redirectPortInput,
|
||||
isRootAvailable = isRootAvailable,
|
||||
isRunning = isRunning,
|
||||
onPortChange = { viewModel.setPort(it) },
|
||||
onRedirectPortChange = { viewModel.setRedirectPort(it) },
|
||||
onApply = { viewModel.applyPortSettings() }
|
||||
)
|
||||
|
||||
// SSL Configuration Card
|
||||
SslConfigCard(
|
||||
sslEnabled = sslEnabled,
|
||||
certPath = sslCertPath,
|
||||
keyPath = sslKeyPath,
|
||||
isRunning = isRunning,
|
||||
onSslEnabledChange = { viewModel.setSslEnabled(it) },
|
||||
onCertPathChange = { viewModel.setSslCertPath(it) },
|
||||
onKeyPathChange = { viewModel.setSslKeyPath(it) },
|
||||
onApply = { viewModel.applySslSettings() }
|
||||
)
|
||||
|
||||
// Settings
|
||||
Text(
|
||||
text = "Settings",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
// Other Settings Card
|
||||
SettingsCard(
|
||||
autoStartEnabled = autoStartEnabled,
|
||||
@@ -144,6 +167,15 @@ fun MainScreen(viewModel: MainViewModel) {
|
||||
},
|
||||
onRefreshBatteryStatus = { viewModel.refreshBatteryOptimizationStatus() }
|
||||
)
|
||||
|
||||
// About
|
||||
Text(
|
||||
text = "About",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
AboutCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,13 +244,14 @@ private fun ServerStatusCard(
|
||||
@Composable
|
||||
private fun PortConfigCard(
|
||||
portInput: String,
|
||||
redirectPortInput: String,
|
||||
isRootAvailable: Boolean,
|
||||
isRunning: Boolean,
|
||||
onPortChange: (String) -> Unit,
|
||||
onRedirectPortChange: (String) -> Unit,
|
||||
onApply: () -> Unit
|
||||
) {
|
||||
val port = portInput.toIntOrNull() ?: 0
|
||||
val needsRoot = port in 1..1023
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@@ -226,7 +259,7 @@ private fun PortConfigCard(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Port Configuration",
|
||||
text = "Port",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -237,28 +270,25 @@ private fun PortConfigCard(
|
||||
value = portInput,
|
||||
onValueChange = { onPortChange(it.filter { c -> c.isDigit() }) },
|
||||
label = { Text("Server Port") },
|
||||
placeholder = { Text("e.g., 80, 443, 8080") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
if (isRootAvailable) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = redirectPortInput,
|
||||
onValueChange = { onRedirectPortChange(it.filter { c -> c.isDigit() }) },
|
||||
label = { Text("Redirect Port (requires root)") },
|
||||
placeholder = { Text("e.g., 80 or 443") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
if (needsRoot) {
|
||||
Text(
|
||||
text = "Ports below 1024 (like 80, 443) require root. Uses iptables to redirect traffic to server port.",
|
||||
text = if (isRootAvailable) {
|
||||
"Port $port requires root access (available)"
|
||||
} else {
|
||||
"Port $port requires root access (not available)"
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = if (isRootAvailable) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.error
|
||||
},
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
@@ -462,3 +492,135 @@ private fun SettingsCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SslConfigCard(
|
||||
sslEnabled: Boolean,
|
||||
certPath: String,
|
||||
keyPath: String,
|
||||
isRunning: Boolean,
|
||||
onSslEnabledChange: (Boolean) -> Unit,
|
||||
onCertPathChange: (String) -> Unit,
|
||||
onKeyPathChange: (String) -> Unit,
|
||||
onApply: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "SSL / HTTPS",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Enable SSL",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = "Coming soon",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = sslEnabled,
|
||||
onCheckedChange = onSslEnabledChange,
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
|
||||
if (sslEnabled) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = certPath,
|
||||
onValueChange = onCertPathChange,
|
||||
label = { Text("Certificate Path") },
|
||||
placeholder = { Text("/sdcard/cert.pem") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = keyPath,
|
||||
onValueChange = onKeyPathChange,
|
||||
label = { Text("Private Key Path") },
|
||||
placeholder = { Text("/sdcard/key.pem") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Provide paths to PEM-encoded certificate and private key files",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onApply,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = certPath.isNotBlank() && keyPath.isNotBlank()
|
||||
) {
|
||||
Text(if (isRunning) "Apply & Restart" else "Save")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AboutCard() {
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Textpipe",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "SMS API Gateway for Android. Send and receive SMS messages via REST API with dual SIM support.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "git.shihaam.dev/shihaam/textpipe",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
modifier = Modifier.clickable {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://git.shihaam.dev/shihaam/textpipe"))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,15 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -36,26 +40,27 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val _portInput = MutableStateFlow(TextpipeService.getConfiguredPort(application).toString())
|
||||
val portInput: StateFlow<String> = _portInput.asStateFlow()
|
||||
|
||||
private val _redirectPortInput = MutableStateFlow("")
|
||||
val redirectPortInput: StateFlow<String> = _redirectPortInput.asStateFlow()
|
||||
|
||||
private val _autoStartEnabled = MutableStateFlow(BootReceiver.isAutoStartEnabled(application))
|
||||
val autoStartEnabled: StateFlow<Boolean> = _autoStartEnabled.asStateFlow()
|
||||
|
||||
private val _isBatteryOptimized = MutableStateFlow(true)
|
||||
val isBatteryOptimized: StateFlow<Boolean> = _isBatteryOptimized.asStateFlow()
|
||||
|
||||
private val _sslEnabled = MutableStateFlow(TextpipeService.isSslEnabled(application))
|
||||
val sslEnabled: StateFlow<Boolean> = _sslEnabled.asStateFlow()
|
||||
|
||||
private val _sslCertPath = MutableStateFlow(TextpipeService.getSslCertPath(application))
|
||||
val sslCertPath: StateFlow<String> = _sslCertPath.asStateFlow()
|
||||
|
||||
private val _sslKeyPath = MutableStateFlow(TextpipeService.getSslKeyPath(application))
|
||||
val sslKeyPath: StateFlow<String> = _sslKeyPath.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
refreshSimInfo()
|
||||
}
|
||||
checkRootAvailability()
|
||||
checkBatteryOptimization()
|
||||
|
||||
val redirectPort = TextpipeService.getRedirectPort(application)
|
||||
if (redirectPort > 0) {
|
||||
_redirectPortInput.value = redirectPort.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSimInfo() {
|
||||
@@ -82,39 +87,47 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
_portInput.value = port
|
||||
}
|
||||
|
||||
fun setRedirectPort(port: String) {
|
||||
_redirectPortInput.value = port
|
||||
}
|
||||
|
||||
fun applyPortSettings() {
|
||||
val port = _portInput.value.toIntOrNull() ?: return
|
||||
if (port !in 1..65535) return
|
||||
|
||||
TextpipeService.setConfiguredPort(getApplication(), port)
|
||||
|
||||
val redirectPort = _redirectPortInput.value.toIntOrNull() ?: 0
|
||||
if (redirectPort in 1..65535 && redirectPort != port) {
|
||||
TextpipeService.setRedirectPort(getApplication(), redirectPort)
|
||||
} else {
|
||||
TextpipeService.setRedirectPort(getApplication(), 0)
|
||||
}
|
||||
|
||||
// Restart service if running
|
||||
// Restart service if running, otherwise just save
|
||||
if (isServerRunning.value) {
|
||||
stopServer()
|
||||
startServer()
|
||||
// Small delay to let service stop before restarting
|
||||
viewModelScope.launch {
|
||||
delay(500)
|
||||
TextpipeService.start(getApplication())
|
||||
showToast("Settings applied, server restarting...")
|
||||
}
|
||||
} else {
|
||||
showToast("Settings saved")
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer() {
|
||||
applyPortSettings()
|
||||
savePortSettings()
|
||||
TextpipeService.start(getApplication())
|
||||
}
|
||||
|
||||
private fun savePortSettings() {
|
||||
val port = _portInput.value.toIntOrNull() ?: return
|
||||
if (port !in 1..65535) return
|
||||
TextpipeService.setConfiguredPort(getApplication(), port)
|
||||
}
|
||||
|
||||
fun stopServer() {
|
||||
TextpipeService.stop(getApplication())
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(getApplication(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleServer() {
|
||||
if (isServerRunning.value) {
|
||||
stopServer()
|
||||
@@ -147,5 +160,35 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun regenerateApiKey(slotIndex: Int) {
|
||||
app.simManager.regenerateApiKey(slotIndex)
|
||||
refreshSimInfo()
|
||||
showToast("API key regenerated for SIM ${slotIndex + 1}")
|
||||
}
|
||||
|
||||
fun setSslEnabled(enabled: Boolean) {
|
||||
_sslEnabled.value = enabled
|
||||
TextpipeService.setSslEnabled(getApplication(), enabled)
|
||||
}
|
||||
|
||||
fun setSslCertPath(path: String) {
|
||||
_sslCertPath.value = path
|
||||
TextpipeService.setSslCertPath(getApplication(), path)
|
||||
}
|
||||
|
||||
fun setSslKeyPath(path: String) {
|
||||
_sslKeyPath.value = path
|
||||
TextpipeService.setSslKeyPath(getApplication(), path)
|
||||
}
|
||||
|
||||
fun applySslSettings() {
|
||||
// Restart service if running to apply SSL changes
|
||||
if (isServerRunning.value) {
|
||||
stopServer()
|
||||
viewModelScope.launch {
|
||||
delay(500)
|
||||
TextpipeService.start(getApplication())
|
||||
showToast("SSL settings applied, server restarting...")
|
||||
}
|
||||
} else {
|
||||
showToast("SSL settings saved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user