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")
|
} ?: 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 NOTIFICATION_ID = 1
|
||||||
private const val PREFS_NAME = "textpipe_settings"
|
private const val PREFS_NAME = "textpipe_settings"
|
||||||
private const val KEY_PORT = "server_port"
|
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 DEFAULT_PORT = 8080
|
||||||
private const val ACTION_STOP = "sh.sar.textpipe.STOP"
|
private const val ACTION_STOP = "sh.sar.textpipe.STOP"
|
||||||
|
|
||||||
@@ -70,14 +72,34 @@ class TextpipeService : Service() {
|
|||||||
prefs.edit().putInt(KEY_PORT, port).apply()
|
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)
|
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)
|
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 var textpipeServer: TextpipeServer? = null
|
||||||
private lateinit var smsRepository: SmsRepository
|
private lateinit var smsRepository: SmsRepository
|
||||||
private lateinit var smsSender: SmsSender
|
private lateinit var smsSender: SmsSender
|
||||||
|
private var usedPrivilegedPort = false
|
||||||
private var redirectPort: Int = 0
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -125,14 +146,18 @@ class TextpipeService : Service() {
|
|||||||
_serverPort.value = port
|
_serverPort.value = port
|
||||||
|
|
||||||
serviceScope.launch(Dispatchers.IO) {
|
serviceScope.launch(Dispatchers.IO) {
|
||||||
// Setup port redirect if configured
|
// If port < 1024, we need root to allow binding
|
||||||
redirectPort = getRedirectPort(this@TextpipeService)
|
if (port < 1024) {
|
||||||
if (redirectPort > 0 && redirectPort != port) {
|
Log.i(TAG, "Port $port requires root access")
|
||||||
val success = RootManager.setupPortRedirect(redirectPort, port)
|
val allowed = RootManager.allowPrivilegedPort(port)
|
||||||
if (!success) {
|
if (allowed) {
|
||||||
Log.w(TAG, "Failed to setup port redirect from $redirectPort to $port")
|
usedPrivilegedPort = true
|
||||||
setRedirectPort(this@TextpipeService, 0)
|
Log.i(TAG, "Privileged port access granted for port $port")
|
||||||
redirectPort = 0
|
} 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)) {
|
when (val result = textpipeServer?.start(port)) {
|
||||||
is ServerStartResult.Success -> {
|
is ServerStartResult.Success -> {
|
||||||
_isRunning.value = true
|
_isRunning.value = true
|
||||||
val displayPort = if (redirectPort > 0) redirectPort else port
|
updateNotification("Running on port $port")
|
||||||
updateNotification("Running on port $displayPort")
|
showToast("Server started on port $port")
|
||||||
showToast("Server started on port $displayPort")
|
|
||||||
Log.i(TAG, "Server started successfully on port $port")
|
Log.i(TAG, "Server started successfully on port $port")
|
||||||
}
|
}
|
||||||
is ServerStartResult.Error -> {
|
is ServerStartResult.Error -> {
|
||||||
@@ -164,12 +188,12 @@ class TextpipeService : Service() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.i(TAG, "Service onDestroy")
|
Log.i(TAG, "Service onDestroy")
|
||||||
|
|
||||||
// Clear port redirect asynchronously (fire and forget to avoid ANR)
|
// Reset privileged port setting if we used it
|
||||||
if (redirectPort > 0) {
|
if (usedPrivilegedPort) {
|
||||||
val port = textpipeServer?.getPort() ?: 8080
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
RootManager.clearPortRedirect(redirectPort, port)
|
RootManager.resetPrivilegedPorts()
|
||||||
}
|
}
|
||||||
|
usedPrivilegedPort = false
|
||||||
}
|
}
|
||||||
|
|
||||||
textpipeServer?.stop()
|
textpipeServer?.stop()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package sh.sar.textpipe.ui
|
package sh.sar.textpipe.ui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
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
|
||||||
@@ -47,6 +50,7 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import sh.sar.textpipe.sim.SimSlotInfo
|
import sh.sar.textpipe.sim.SimSlotInfo
|
||||||
|
|
||||||
@@ -57,9 +61,11 @@ fun MainScreen(viewModel: MainViewModel) {
|
|||||||
val simSlots by viewModel.simSlots.collectAsState()
|
val simSlots by viewModel.simSlots.collectAsState()
|
||||||
val isRootAvailable by viewModel.isRootAvailable.collectAsState()
|
val isRootAvailable by viewModel.isRootAvailable.collectAsState()
|
||||||
val portInput by viewModel.portInput.collectAsState()
|
val portInput by viewModel.portInput.collectAsState()
|
||||||
val redirectPortInput by viewModel.redirectPortInput.collectAsState()
|
|
||||||
val autoStartEnabled by viewModel.autoStartEnabled.collectAsState()
|
val autoStartEnabled by viewModel.autoStartEnabled.collectAsState()
|
||||||
val isBatteryOptimized by viewModel.isBatteryOptimized.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 context = LocalContext.current
|
||||||
val activity = context as? Activity
|
val activity = context as? Activity
|
||||||
@@ -116,9 +122,9 @@ fun MainScreen(viewModel: MainViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// Server Configuration
|
||||||
Text(
|
Text(
|
||||||
text = "Settings",
|
text = "Server Configuration",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -126,14 +132,31 @@ fun MainScreen(viewModel: MainViewModel) {
|
|||||||
// Port Configuration Card
|
// Port Configuration Card
|
||||||
PortConfigCard(
|
PortConfigCard(
|
||||||
portInput = portInput,
|
portInput = portInput,
|
||||||
redirectPortInput = redirectPortInput,
|
|
||||||
isRootAvailable = isRootAvailable,
|
isRootAvailable = isRootAvailable,
|
||||||
isRunning = isRunning,
|
isRunning = isRunning,
|
||||||
onPortChange = { viewModel.setPort(it) },
|
onPortChange = { viewModel.setPort(it) },
|
||||||
onRedirectPortChange = { viewModel.setRedirectPort(it) },
|
|
||||||
onApply = { viewModel.applyPortSettings() }
|
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
|
// Other Settings Card
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
autoStartEnabled = autoStartEnabled,
|
autoStartEnabled = autoStartEnabled,
|
||||||
@@ -144,6 +167,15 @@ fun MainScreen(viewModel: MainViewModel) {
|
|||||||
},
|
},
|
||||||
onRefreshBatteryStatus = { viewModel.refreshBatteryOptimizationStatus() }
|
onRefreshBatteryStatus = { viewModel.refreshBatteryOptimizationStatus() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// About
|
||||||
|
Text(
|
||||||
|
text = "About",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
AboutCard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,13 +244,14 @@ private fun ServerStatusCard(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PortConfigCard(
|
private fun PortConfigCard(
|
||||||
portInput: String,
|
portInput: String,
|
||||||
redirectPortInput: String,
|
|
||||||
isRootAvailable: Boolean,
|
isRootAvailable: Boolean,
|
||||||
isRunning: Boolean,
|
isRunning: Boolean,
|
||||||
onPortChange: (String) -> Unit,
|
onPortChange: (String) -> Unit,
|
||||||
onRedirectPortChange: (String) -> Unit,
|
|
||||||
onApply: () -> Unit
|
onApply: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val port = portInput.toIntOrNull() ?: 0
|
||||||
|
val needsRoot = port in 1..1023
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
@@ -226,7 +259,7 @@ private fun PortConfigCard(
|
|||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Port Configuration",
|
text = "Port",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -237,28 +270,25 @@ private fun PortConfigCard(
|
|||||||
value = portInput,
|
value = portInput,
|
||||||
onValueChange = { onPortChange(it.filter { c -> c.isDigit() }) },
|
onValueChange = { onPortChange(it.filter { c -> c.isDigit() }) },
|
||||||
label = { Text("Server Port") },
|
label = { Text("Server Port") },
|
||||||
|
placeholder = { Text("e.g., 80, 443, 8080") },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isRootAvailable) {
|
if (needsRoot) {
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isRootAvailable) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
},
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -36,26 +40,27 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val _portInput = MutableStateFlow(TextpipeService.getConfiguredPort(application).toString())
|
private val _portInput = MutableStateFlow(TextpipeService.getConfiguredPort(application).toString())
|
||||||
val portInput: StateFlow<String> = _portInput.asStateFlow()
|
val portInput: StateFlow<String> = _portInput.asStateFlow()
|
||||||
|
|
||||||
private val _redirectPortInput = MutableStateFlow("")
|
|
||||||
val redirectPortInput: StateFlow<String> = _redirectPortInput.asStateFlow()
|
|
||||||
|
|
||||||
private val _autoStartEnabled = MutableStateFlow(BootReceiver.isAutoStartEnabled(application))
|
private val _autoStartEnabled = MutableStateFlow(BootReceiver.isAutoStartEnabled(application))
|
||||||
val autoStartEnabled: StateFlow<Boolean> = _autoStartEnabled.asStateFlow()
|
val autoStartEnabled: StateFlow<Boolean> = _autoStartEnabled.asStateFlow()
|
||||||
|
|
||||||
private val _isBatteryOptimized = MutableStateFlow(true)
|
private val _isBatteryOptimized = MutableStateFlow(true)
|
||||||
val isBatteryOptimized: StateFlow<Boolean> = _isBatteryOptimized.asStateFlow()
|
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 {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
refreshSimInfo()
|
refreshSimInfo()
|
||||||
}
|
}
|
||||||
checkRootAvailability()
|
checkRootAvailability()
|
||||||
checkBatteryOptimization()
|
checkBatteryOptimization()
|
||||||
|
|
||||||
val redirectPort = TextpipeService.getRedirectPort(application)
|
|
||||||
if (redirectPort > 0) {
|
|
||||||
_redirectPortInput.value = redirectPort.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshSimInfo() {
|
fun refreshSimInfo() {
|
||||||
@@ -82,39 +87,47 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_portInput.value = port
|
_portInput.value = port
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRedirectPort(port: String) {
|
|
||||||
_redirectPortInput.value = port
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyPortSettings() {
|
fun applyPortSettings() {
|
||||||
val port = _portInput.value.toIntOrNull() ?: return
|
val port = _portInput.value.toIntOrNull() ?: return
|
||||||
if (port !in 1..65535) return
|
if (port !in 1..65535) return
|
||||||
|
|
||||||
TextpipeService.setConfiguredPort(getApplication(), port)
|
TextpipeService.setConfiguredPort(getApplication(), port)
|
||||||
|
|
||||||
val redirectPort = _redirectPortInput.value.toIntOrNull() ?: 0
|
// Restart service if running, otherwise just save
|
||||||
if (redirectPort in 1..65535 && redirectPort != port) {
|
|
||||||
TextpipeService.setRedirectPort(getApplication(), redirectPort)
|
|
||||||
} else {
|
|
||||||
TextpipeService.setRedirectPort(getApplication(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart service if running
|
|
||||||
if (isServerRunning.value) {
|
if (isServerRunning.value) {
|
||||||
stopServer()
|
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() {
|
fun startServer() {
|
||||||
applyPortSettings()
|
savePortSettings()
|
||||||
TextpipeService.start(getApplication())
|
TextpipeService.start(getApplication())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun savePortSettings() {
|
||||||
|
val port = _portInput.value.toIntOrNull() ?: return
|
||||||
|
if (port !in 1..65535) return
|
||||||
|
TextpipeService.setConfiguredPort(getApplication(), port)
|
||||||
|
}
|
||||||
|
|
||||||
fun stopServer() {
|
fun stopServer() {
|
||||||
TextpipeService.stop(getApplication())
|
TextpipeService.stop(getApplication())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showToast(message: String) {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
Toast.makeText(getApplication(), message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleServer() {
|
fun toggleServer() {
|
||||||
if (isServerRunning.value) {
|
if (isServerRunning.value) {
|
||||||
stopServer()
|
stopServer()
|
||||||
@@ -147,5 +160,35 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun regenerateApiKey(slotIndex: Int) {
|
fun regenerateApiKey(slotIndex: Int) {
|
||||||
app.simManager.regenerateApiKey(slotIndex)
|
app.simManager.regenerateApiKey(slotIndex)
|
||||||
refreshSimInfo()
|
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 {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user