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:
2026-03-22 22:04:14 +08:00
parent 4b09fe817d
commit 21621b15f4
13 changed files with 602 additions and 208 deletions

View File

@@ -27,6 +27,10 @@
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:exported="false" />
<service
android:name=".service.TunnelService"
android:exported="false"

View File

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

View File

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

View File

@@ -5,5 +5,4 @@ data class AppConfig(
val token: String = "",
val autoStart: Boolean = true,
val autoReconnect: Boolean = true,
val useTls: Boolean = true,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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

View File

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

View File

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