feat: Add SettingsActivity for configuration management and logging
- Introduced SettingsActivity to manage server address and token settings. - Integrated LogStore for logging status updates and messages. - Updated MainActivity to navigate to SettingsActivity and handle configuration. - Modified UI in activity_main.xml and activity_settings.xml for improved user experience. - Adjusted color scheme in colors.xml for better visibility and aesthetics. - Enhanced string resources in strings.xml for clarity and consistency. - Refactored notification handling in NotificationHelper.kt to use status labels. - Updated TunnelService to log status changes and messages.
This commit is contained in:
@@ -27,6 +27,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.TunnelService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -3,17 +3,14 @@ package com.gotunnel.android
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.gotunnel.android.bridge.TunnelStatus
|
||||
import com.gotunnel.android.config.AppConfig
|
||||
import com.gotunnel.android.config.ConfigStore
|
||||
import com.gotunnel.android.config.LogStore
|
||||
import com.gotunnel.android.config.ServiceStateStore
|
||||
import com.gotunnel.android.databinding.ActivityMainBinding
|
||||
import com.gotunnel.android.service.TunnelService
|
||||
@@ -24,6 +21,8 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var configStore: ConfigStore
|
||||
private lateinit var stateStore: ServiceStateStore
|
||||
private lateinit var logStore: LogStore
|
||||
|
||||
private val notificationPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (!granted) {
|
||||
@@ -33,7 +32,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
requestNotificationPermissionIfNeeded()
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
@@ -41,21 +39,20 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
configStore = ConfigStore(this)
|
||||
stateStore = ServiceStateStore(this)
|
||||
logStore = LogStore(this)
|
||||
|
||||
populateForm(configStore.load())
|
||||
renderState()
|
||||
|
||||
binding.saveButton.setOnClickListener {
|
||||
val config = readForm()
|
||||
configStore.save(config)
|
||||
renderState()
|
||||
Toast.makeText(this, R.string.config_saved, Toast.LENGTH_SHORT).show()
|
||||
binding.topToolbar.setNavigationOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
binding.startButton.setOnClickListener {
|
||||
val config = readForm()
|
||||
configStore.save(config)
|
||||
renderState()
|
||||
val config = configStore.load()
|
||||
if (config.serverAddress.isBlank() || config.token.isBlank()) {
|
||||
Toast.makeText(this, R.string.config_missing, Toast.LENGTH_SHORT).show()
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
ContextCompat.startForegroundService(
|
||||
this,
|
||||
TunnelService.createStartIntent(this, "manual-start"),
|
||||
@@ -70,36 +67,15 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
Toast.makeText(this, R.string.service_stop_requested, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
binding.batteryButton.setOnClickListener {
|
||||
openBatteryOptimizationSettings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
renderState()
|
||||
renderScreen()
|
||||
}
|
||||
|
||||
private fun populateForm(config: AppConfig) {
|
||||
binding.serverAddressInput.setText(config.serverAddress)
|
||||
binding.tokenInput.setText(config.token)
|
||||
binding.autoStartSwitch.isChecked = config.autoStart
|
||||
binding.autoReconnectSwitch.isChecked = config.autoReconnect
|
||||
binding.useTlsSwitch.isChecked = config.useTls
|
||||
}
|
||||
|
||||
private fun readForm(): AppConfig {
|
||||
return AppConfig(
|
||||
serverAddress = binding.serverAddressInput.text?.toString().orEmpty().trim(),
|
||||
token = binding.tokenInput.text?.toString().orEmpty().trim(),
|
||||
autoStart = binding.autoStartSwitch.isChecked,
|
||||
autoReconnect = binding.autoReconnectSwitch.isChecked,
|
||||
useTls = binding.useTlsSwitch.isChecked,
|
||||
)
|
||||
}
|
||||
|
||||
private fun renderState() {
|
||||
private fun renderScreen() {
|
||||
val config = configStore.load()
|
||||
val state = stateStore.load()
|
||||
val timestamp = if (state.updatedAt > 0L) {
|
||||
DateFormat.getDateTimeInstance().format(Date(state.updatedAt))
|
||||
@@ -107,21 +83,37 @@ class MainActivity : AppCompatActivity() {
|
||||
getString(R.string.state_never_updated)
|
||||
}
|
||||
|
||||
binding.stateValue.text = getString(
|
||||
R.string.state_format,
|
||||
state.status.name,
|
||||
state.detail.ifBlank { getString(R.string.state_no_detail) },
|
||||
)
|
||||
binding.stateMeta.text = getString(R.string.state_meta_format, timestamp)
|
||||
binding.statusValue.text = getStatusLabel(state.status)
|
||||
binding.statusDetail.text = state.detail.ifBlank { getString(R.string.state_no_detail) }
|
||||
binding.statusMeta.text = getString(R.string.state_meta_format, timestamp)
|
||||
binding.stateHint.text = getStateHint(state.status)
|
||||
binding.serverSummary.text = if (config.serverAddress.isBlank()) {
|
||||
getString(R.string.status_server_unconfigured)
|
||||
} else {
|
||||
getString(R.string.status_server_configured, config.serverAddress)
|
||||
}
|
||||
binding.logValue.text = logStore.render()
|
||||
}
|
||||
|
||||
val hint = when (state.status) {
|
||||
private fun getStatusLabel(status: TunnelStatus): String {
|
||||
return when (status) {
|
||||
TunnelStatus.RUNNING -> getString(R.string.status_running)
|
||||
TunnelStatus.STARTING -> getString(R.string.status_starting)
|
||||
TunnelStatus.RECONNECTING -> getString(R.string.status_reconnecting)
|
||||
TunnelStatus.ERROR -> getString(R.string.status_error)
|
||||
TunnelStatus.STOPPED -> getString(R.string.status_stopped)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStateHint(status: TunnelStatus): String {
|
||||
val messageId = when (status) {
|
||||
TunnelStatus.RUNNING -> R.string.state_hint_running
|
||||
TunnelStatus.STARTING -> R.string.state_hint_starting
|
||||
TunnelStatus.RECONNECTING -> R.string.state_hint_reconnecting
|
||||
TunnelStatus.ERROR -> R.string.state_hint_error
|
||||
TunnelStatus.STOPPED -> R.string.state_hint_stopped
|
||||
}
|
||||
binding.stateHint.text = getString(hint)
|
||||
return getString(messageId)
|
||||
}
|
||||
|
||||
private fun requestNotificationPermissionIfNeeded() {
|
||||
@@ -137,17 +129,4 @@ class MainActivity : AppCompatActivity() {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openBatteryOptimizationSettings() {
|
||||
val powerManager = getSystemService(PowerManager::class.java)
|
||||
if (powerManager != null && powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Toast.makeText(this, R.string.battery_optimization_already_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.gotunnel.android
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.gotunnel.android.config.AppConfig
|
||||
import com.gotunnel.android.config.ConfigStore
|
||||
import com.gotunnel.android.databinding.ActivitySettingsBinding
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private lateinit var configStore: ConfigStore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
configStore = ConfigStore(this)
|
||||
populateForm(configStore.load())
|
||||
|
||||
binding.topToolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.saveButton.setOnClickListener {
|
||||
configStore.save(readForm())
|
||||
Toast.makeText(this, R.string.config_saved, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.batteryButton.setOnClickListener {
|
||||
openBatteryOptimizationSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateForm(config: AppConfig) {
|
||||
binding.serverAddressInput.setText(config.serverAddress)
|
||||
binding.tokenInput.setText(config.token)
|
||||
binding.autoStartSwitch.isChecked = config.autoStart
|
||||
binding.autoReconnectSwitch.isChecked = config.autoReconnect
|
||||
}
|
||||
|
||||
private fun readForm(): AppConfig {
|
||||
return AppConfig(
|
||||
serverAddress = binding.serverAddressInput.text?.toString().orEmpty().trim(),
|
||||
token = binding.tokenInput.text?.toString().orEmpty().trim(),
|
||||
autoStart = binding.autoStartSwitch.isChecked,
|
||||
autoReconnect = binding.autoReconnectSwitch.isChecked,
|
||||
)
|
||||
}
|
||||
|
||||
private fun openBatteryOptimizationSettings() {
|
||||
val powerManager = getSystemService(PowerManager::class.java)
|
||||
if (powerManager != null && powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Toast.makeText(this, R.string.battery_optimization_already_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ data class AppConfig(
|
||||
val token: String = "",
|
||||
val autoStart: Boolean = true,
|
||||
val autoReconnect: Boolean = true,
|
||||
val useTls: Boolean = true,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ class ConfigStore(context: Context) {
|
||||
token = prefs.getString(KEY_TOKEN, "") ?: "",
|
||||
autoStart = prefs.getBoolean(KEY_AUTO_START, true),
|
||||
autoReconnect = prefs.getBoolean(KEY_AUTO_RECONNECT, true),
|
||||
useTls = prefs.getBoolean(KEY_USE_TLS, true),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +20,7 @@ class ConfigStore(context: Context) {
|
||||
.putString(KEY_TOKEN, config.token)
|
||||
.putBoolean(KEY_AUTO_START, config.autoStart)
|
||||
.putBoolean(KEY_AUTO_RECONNECT, config.autoReconnect)
|
||||
.putBoolean(KEY_USE_TLS, config.useTls)
|
||||
.remove(KEY_USE_TLS)
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.gotunnel.android.config
|
||||
|
||||
import android.content.Context
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class LogStore(context: Context) {
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun append(message: String) {
|
||||
if (message.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val current = load().toMutableList()
|
||||
current += "${timestamp()} $message"
|
||||
while (current.size > MAX_LINES) {
|
||||
current.removeAt(0)
|
||||
}
|
||||
|
||||
prefs.edit().putString(KEY_LOGS, current.joinToString(SEPARATOR)).apply()
|
||||
}
|
||||
|
||||
fun load(): List<String> {
|
||||
val raw = prefs.getString(KEY_LOGS, "") ?: ""
|
||||
if (raw.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
return raw.split(SEPARATOR).filter { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun render(): String {
|
||||
val lines = load()
|
||||
return if (lines.isEmpty()) {
|
||||
"No logs yet."
|
||||
} else {
|
||||
lines.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun timestamp(): String {
|
||||
return SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "gotunnel_logs"
|
||||
private const val KEY_LOGS = "logs"
|
||||
private const val MAX_LINES = 80
|
||||
private const val SEPARATOR = "\u0001"
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ object NotificationHelper {
|
||||
|
||||
return NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_gotunnel_notification)
|
||||
.setContentTitle(context.getString(R.string.notification_title, status.name))
|
||||
.setContentTitle(context.getString(R.string.notification_title, statusLabel(context, status)))
|
||||
.setContentText(baseText)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(baseText))
|
||||
.setOngoing(status != TunnelStatus.STOPPED)
|
||||
@@ -86,6 +86,16 @@ object NotificationHelper {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun statusLabel(context: Context, status: TunnelStatus): String {
|
||||
return when (status) {
|
||||
TunnelStatus.RUNNING -> context.getString(R.string.status_running)
|
||||
TunnelStatus.STARTING -> context.getString(R.string.status_starting)
|
||||
TunnelStatus.RECONNECTING -> context.getString(R.string.status_reconnecting)
|
||||
TunnelStatus.ERROR -> context.getString(R.string.status_error)
|
||||
TunnelStatus.STOPPED -> context.getString(R.string.status_stopped)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingIntentFlags(): Int {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.gotunnel.android.bridge.TunnelController
|
||||
import com.gotunnel.android.bridge.TunnelStatus
|
||||
import com.gotunnel.android.config.AppConfig
|
||||
import com.gotunnel.android.config.ConfigStore
|
||||
import com.gotunnel.android.config.LogStore
|
||||
import com.gotunnel.android.config.ServiceStateStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -20,6 +21,7 @@ class TunnelService : Service() {
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
private lateinit var configStore: ConfigStore
|
||||
private lateinit var stateStore: ServiceStateStore
|
||||
private lateinit var logStore: LogStore
|
||||
private lateinit var controller: TunnelController
|
||||
private lateinit var networkMonitor: NetworkMonitor
|
||||
private var currentConfig: AppConfig = AppConfig()
|
||||
@@ -29,16 +31,18 @@ class TunnelService : Service() {
|
||||
super.onCreate()
|
||||
configStore = ConfigStore(this)
|
||||
stateStore = ServiceStateStore(this)
|
||||
logStore = LogStore(this)
|
||||
controller = GoTunnelBridge.create(applicationContext)
|
||||
controller.setListener(object : TunnelController.Listener {
|
||||
override fun onStatusChanged(status: TunnelStatus, detail: String) {
|
||||
stateStore.save(status, detail)
|
||||
logStore.append("status: ${status.name} ${detail.ifBlank { "" }}".trim())
|
||||
updateNotification(status, detail)
|
||||
}
|
||||
|
||||
override fun onLog(message: String) {
|
||||
val current = stateStore.load()
|
||||
stateStore.save(current.status, message)
|
||||
logStore.append(message)
|
||||
updateNotification(current.status, message)
|
||||
}
|
||||
})
|
||||
@@ -57,6 +61,7 @@ class TunnelService : Service() {
|
||||
onLost = {
|
||||
val detail = getString(com.gotunnel.android.R.string.network_lost)
|
||||
stateStore.save(TunnelStatus.RECONNECTING, detail)
|
||||
logStore.append(detail)
|
||||
updateNotification(TunnelStatus.RECONNECTING, detail)
|
||||
},
|
||||
)
|
||||
@@ -98,12 +103,7 @@ class TunnelService : Service() {
|
||||
NotificationHelper.ensureChannel(this)
|
||||
startForeground(
|
||||
NotificationHelper.NOTIFICATION_ID,
|
||||
NotificationHelper.build(
|
||||
this,
|
||||
state.status,
|
||||
state.detail,
|
||||
config,
|
||||
),
|
||||
NotificationHelper.build(this, state.status, state.detail, config),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,11 +111,13 @@ class TunnelService : Service() {
|
||||
currentConfig = configStore.load()
|
||||
controller.updateConfig(currentConfig)
|
||||
stateStore.save(TunnelStatus.STARTING, reason)
|
||||
logStore.append("start requested: $reason")
|
||||
updateNotification(TunnelStatus.STARTING, reason)
|
||||
|
||||
if (!isConfigReady(currentConfig)) {
|
||||
val detail = getString(com.gotunnel.android.R.string.config_missing)
|
||||
stateStore.save(TunnelStatus.STOPPED, detail)
|
||||
logStore.append(detail)
|
||||
updateNotification(TunnelStatus.STOPPED, detail)
|
||||
return
|
||||
}
|
||||
@@ -130,6 +132,7 @@ class TunnelService : Service() {
|
||||
networkMonitorPrimed = false
|
||||
controller.stop(reason)
|
||||
stateStore.save(TunnelStatus.STOPPED, reason)
|
||||
logStore.append("stop requested: $reason")
|
||||
updateNotification(TunnelStatus.STOPPED, reason)
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
|
||||
@@ -1,120 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/gotunnel_background"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/topToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineMedium" />
|
||||
android:background="@android:color/transparent"
|
||||
app:navigationIcon="@android:drawable/ic_menu_manage"
|
||||
app:navigationIconTint="@color/gotunnel_text"
|
||||
app:subtitle="@string/main_subtitle"
|
||||
app:subtitleTextColor="@color/gotunnel_text_muted"
|
||||
app:title="@string/home_title"
|
||||
app:titleTextColor="@color/gotunnel_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/main_subtitle"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/serverAddressInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:hint="@string/server_address_hint"
|
||||
android:inputType="textUri"
|
||||
android:padding="12dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/tokenInput"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/token_hint"
|
||||
android:inputType="textPassword"
|
||||
android:padding="12dp" />
|
||||
app:cardBackgroundColor="@color/gotunnel_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:strokeColor="@color/gotunnel_border"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/autoStartSwitch"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/auto_start_label" />
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/autoReconnectSwitch"
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status_card_title"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusDetail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/auto_reconnect_label" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/useTlsSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_tls_label" />
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stateHint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/state_hint_stopped"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stateValue"
|
||||
android:id="@+id/serverSummary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||
android:layout_marginTop="14dp"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stateMeta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/batteryButton"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/battery_button" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/save_button" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/startButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/start_button" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/stopButton"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
@@ -122,4 +110,114 @@
|
||||
android:text="@string/stop_button" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
app:cardBackgroundColor="@color/gotunnel_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:strokeColor="@color/gotunnel_border"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/log_card_title"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/logValue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
app:cardBackgroundColor="@color/gotunnel_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:strokeColor="@color/gotunnel_border"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/proxy_card_title"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/proxy_card_subtitle"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
app:chipSpacingHorizontal="8dp"
|
||||
app:chipSpacingVertical="8dp"
|
||||
app:singleLine="false">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TCP" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="UDP" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="HTTP" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="HTTPS" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SOCKS5" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
156
android/app/src/main/res/layout/activity_settings.xml
Normal file
156
android/app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,156 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/gotunnel_background"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/topToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
app:navigationIcon="@android:drawable/ic_media_previous"
|
||||
app:navigationIconTint="@color/gotunnel_text"
|
||||
app:subtitle="@string/settings_subtitle"
|
||||
app:subtitleTextColor="@color/gotunnel_text_muted"
|
||||
app:title="@string/settings_title"
|
||||
app:titleTextColor="@color/gotunnel_text" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
app:cardBackgroundColor="@color/gotunnel_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:strokeColor="@color/gotunnel_border"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_basic_title"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:hint="@string/server_address_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:helperText="@string/server_address_helper">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/serverAddressInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:hint="@string/token_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:helperText="@string/token_helper">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/tokenInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
app:cardBackgroundColor="@color/gotunnel_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:strokeColor="@color/gotunnel_border"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_behavior_title"
|
||||
android:textColor="@color/gotunnel_text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/autoStartSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:text="@string/auto_start_label"
|
||||
android:textColor="@color/gotunnel_text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/auto_start_helper"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/autoReconnectSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:text="@string/auto_reconnect_label"
|
||||
android:textColor="@color/gotunnel_text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/auto_reconnect_helper"
|
||||
android:textColor="@color/gotunnel_text_muted"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/batteryButton"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:text="@string/battery_button" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/save_button" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -1,7 +1,9 @@
|
||||
<resources>
|
||||
<color name="gotunnel_background">#0F172A</color>
|
||||
<color name="gotunnel_surface">#111827</color>
|
||||
<color name="gotunnel_primary">#0EA5A8</color>
|
||||
<color name="gotunnel_secondary">#38BDF8</color>
|
||||
<color name="gotunnel_text">#E5E7EB</color>
|
||||
<color name="gotunnel_background">#F3F7FB</color>
|
||||
<color name="gotunnel_surface">#FFFFFF</color>
|
||||
<color name="gotunnel_primary">#0F766E</color>
|
||||
<color name="gotunnel_secondary">#0891B2</color>
|
||||
<color name="gotunnel_text">#0F172A</color>
|
||||
<color name="gotunnel_text_muted">#475569</color>
|
||||
<color name="gotunnel_border">#D9E2EC</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
<resources>
|
||||
<string name="app_name">GoTunnel</string>
|
||||
<string name="main_subtitle">Android host shell for the GoTunnel client core.</string>
|
||||
<string name="server_address_hint">Server address, for example 10.0.2.2:7000</string>
|
||||
<string name="token_hint">Authentication token</string>
|
||||
<string name="home_title">GoTunnel Client</string>
|
||||
<string name="main_subtitle">Status, recent logs, and supported proxy types</string>
|
||||
<string name="settings_title">Client Settings</string>
|
||||
<string name="settings_subtitle">Connection settings and startup behavior</string>
|
||||
<string name="settings_basic_title">Basic Connection</string>
|
||||
<string name="settings_behavior_title">Startup and Recovery</string>
|
||||
<string name="server_address_label">Server Address</string>
|
||||
<string name="server_address_helper">Example: 1.2.3.4:7000. This is the GoTunnel server endpoint.</string>
|
||||
<string name="token_label">Access Token</string>
|
||||
<string name="token_helper">The token issued by the server to identify this client.</string>
|
||||
<string name="auto_start_label">Start on boot</string>
|
||||
<string name="auto_reconnect_label">Restart on network restore</string>
|
||||
<string name="use_tls_label">Use TLS</string>
|
||||
<string name="save_button">Save</string>
|
||||
<string name="start_button">Start</string>
|
||||
<string name="stop_button">Stop</string>
|
||||
<string name="battery_button">Battery optimization</string>
|
||||
<string name="config_saved">Configuration saved</string>
|
||||
<string name="service_start_requested">Service start requested</string>
|
||||
<string name="service_stop_requested">Service stop requested</string>
|
||||
<string name="auto_start_helper">Restore the foreground service after reboot or app update.</string>
|
||||
<string name="auto_reconnect_label">Reconnect when network returns</string>
|
||||
<string name="auto_reconnect_helper">Restart the client automatically after connectivity is restored.</string>
|
||||
<string name="save_button">Save Settings</string>
|
||||
<string name="start_button">Start Client</string>
|
||||
<string name="stop_button">Stop Client</string>
|
||||
<string name="battery_button">Battery Optimization</string>
|
||||
<string name="status_card_title">Software Status</string>
|
||||
<string name="log_card_title">Recent Logs</string>
|
||||
<string name="proxy_card_title">Supported Proxies</string>
|
||||
<string name="proxy_card_subtitle">The current Android shell can present these proxy types. Live proxy rules can be shown after the native Go core is connected.</string>
|
||||
<string name="config_saved">Client settings saved.</string>
|
||||
<string name="service_start_requested">Client start requested.</string>
|
||||
<string name="service_stop_requested">Client stop requested.</string>
|
||||
<string name="battery_optimization_already_disabled">Battery optimization is already disabled for GoTunnel.</string>
|
||||
<string name="notification_permission_denied">Notification permission was denied. Foreground service notifications may be limited.</string>
|
||||
<string name="state_never_updated">Never updated</string>
|
||||
<string name="state_no_detail">No detail</string>
|
||||
<string name="state_format">State: %1$s\nDetail: %2$s</string>
|
||||
<string name="state_meta_format">Updated: %1$s</string>
|
||||
<string name="state_hint_stopped">The foreground service is idle until a configuration is saved and started.</string>
|
||||
<string name="state_hint_starting">The host shell is preparing a tunnel session.</string>
|
||||
<string name="state_hint_running">The host shell is ready. The native Go tunnel core can be attached here later.</string>
|
||||
<string name="state_hint_reconnecting">The host shell is waiting for connectivity to return.</string>
|
||||
<string name="state_hint_error">The last session reported an error. Check the configuration and service notification.</string>
|
||||
<string name="notification_channel_name">GoTunnel service</string>
|
||||
<string name="notification_channel_description">Keeps the Android host shell running in the foreground</string>
|
||||
<string name="state_never_updated">Not updated yet</string>
|
||||
<string name="state_no_detail">No detail available</string>
|
||||
<string name="state_meta_format">Last update: %1$s</string>
|
||||
<string name="state_hint_stopped">The client is idle. Save settings first, then start it from the home screen.</string>
|
||||
<string name="state_hint_starting">The client is preparing a tunnel connection.</string>
|
||||
<string name="state_hint_running">The client is running and ready for the native Go core to handle real proxy traffic.</string>
|
||||
<string name="state_hint_reconnecting">The client is waiting for the network and will reconnect automatically.</string>
|
||||
<string name="state_hint_error">The last run failed. Check the settings and recent logs.</string>
|
||||
<string name="status_running">Running</string>
|
||||
<string name="status_starting">Starting</string>
|
||||
<string name="status_reconnecting">Reconnecting</string>
|
||||
<string name="status_error">Error</string>
|
||||
<string name="status_stopped">Stopped</string>
|
||||
<string name="status_server_unconfigured">No server is configured yet. Open Settings to finish the basic client setup.</string>
|
||||
<string name="status_server_configured">Current server: %1$s</string>
|
||||
<string name="notification_channel_name">GoTunnel foreground service</string>
|
||||
<string name="notification_channel_description">Keeps the GoTunnel Android client running in the foreground</string>
|
||||
<string name="notification_title">GoTunnel - %1$s</string>
|
||||
<string name="notification_text_configured">Configured for %1$s</string>
|
||||
<string name="notification_text_configured">Current target: %1$s</string>
|
||||
<string name="notification_text_unconfigured">No server configured yet</string>
|
||||
<string name="notification_action_restart">Restart</string>
|
||||
<string name="notification_action_stop">Stop</string>
|
||||
<string name="config_missing">Server address and token are required</string>
|
||||
<string name="network_lost">Network lost</string>
|
||||
<string name="config_missing">Please open Settings and fill in the server address and access token first.</string>
|
||||
<string name="network_lost">Network connection lost</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.GoTunnel" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<style name="Theme.GoTunnel" parent="Theme.Material3.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/gotunnel_primary</item>
|
||||
<item name="colorOnPrimary">@android:color/white</item>
|
||||
<item name="colorSecondary">@color/gotunnel_secondary</item>
|
||||
<item name="colorOnSecondary">@android:color/white</item>
|
||||
<item name="colorSurface">@color/gotunnel_surface</item>
|
||||
<item name="colorOnSurface">@color/gotunnel_text</item>
|
||||
<item name="colorOutline">@color/gotunnel_border</item>
|
||||
<item name="android:colorBackground">@color/gotunnel_background</item>
|
||||
<item name="android:windowBackground">@color/gotunnel_background</item>
|
||||
<item name="android:textColorPrimary">@color/gotunnel_text</item>
|
||||
<item name="android:textColorSecondary">@color/gotunnel_text</item>
|
||||
<item name="android:textColorSecondary">@color/gotunnel_text_muted</item>
|
||||
<item name="android:statusBarColor" tools:targetApi="l">@color/gotunnel_background</item>
|
||||
<item name="android:navigationBarColor" tools:targetApi="l">@color/gotunnel_background</item>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user