rearrange Ui, prep for ssl support, removed iptables and resorting to use syscalls to allow ports bwlo 1024

This commit is contained in:
2026-02-06 01:53:31 +05:00
parent 23122d32a9
commit 4142606728
5 changed files with 345 additions and 67 deletions

View File

@@ -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
}
} }

View File

@@ -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()

View File

@@ -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)
}
)
}
}
}

View File

@@ -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")
}
} }
} }

View File

@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://jitpack.io") }
} }
} }