From e6ee0f6cebf481b2ec9661fd11ca6a9f3cc8db7d Mon Sep 17 00:00:00 2001 From: pyamsoft Date: Mon, 30 Dec 2024 20:37:17 -0800 Subject: [PATCH] Add DEV UI support for SOCKS proxy port --- .../com/pyamsoft/tetherfi/PreferencesImpl.kt | 6 +- .../com/pyamsoft/tetherfi/info/InfoEntry.kt | 3 + .../com/pyamsoft/tetherfi/main/MainContent.kt | 4 + .../com/pyamsoft/tetherfi/main/MainEntry.kt | 7 +- .../com/pyamsoft/tetherfi/main/MainScreen.kt | 9 +- .../pyamsoft/tetherfi/status/StatusEntry.kt | 6 +- .../com/pyamsoft/tetherfi/info/InfoScreen.kt | 5 + .../info/RenderConnectionInstructions.kt | 5 + .../info/sections/RenderDeviceSetup.kt | 246 +++++++++++++----- .../values-zh-rTW/devivce_setup_strings.xml | 1 - .../main/res/values/devivce_setup_strings.xml | 21 +- .../pyamsoft/tetherfi/main/MainViewModeler.kt | 6 +- .../pyamsoft/tetherfi/main/MainViewState.kt | 5 +- .../factory/DefaultProxyManagerFactory.kt | 4 +- .../tetherfi/status/StatusLoadedContent.kt | 16 +- .../pyamsoft/tetherfi/status/StatusScreen.kt | 16 +- .../tetherfi/status/StatusViewModeler.kt | 42 ++- .../tetherfi/status/StatusViewState.kt | 11 +- .../sections/network/EditModeWidgets.kt | 44 +++- .../sections/network/NetworkInformation.kt | 16 +- .../sections/network/RenderEditableItems.kt | 40 ++- .../sections/network/RenderRunningItems.kt | 2 +- .../sections/network/ViewModeWidgets.kt | 57 ++-- .../res/values-zh-rTW/edit_mode_strings.xml | 2 - .../res/values-zh-rTW/view_mode_strings.xml | 1 - .../src/main/res/values/edit_mode_strings.xml | 7 +- .../src/main/res/values/view_mode_strings.xml | 5 +- ui/src/main/java/PortFormatter.kt | 33 +++ .../pyamsoft/tetherfi/ui/ServerViewState.kt | 3 +- .../tetherfi/ui/test/TestServerViewState.kt | 21 +- 30 files changed, 493 insertions(+), 151 deletions(-) create mode 100644 ui/src/main/java/PortFormatter.kt diff --git a/app/src/main/java/com/pyamsoft/tetherfi/PreferencesImpl.kt b/app/src/main/java/com/pyamsoft/tetherfi/PreferencesImpl.kt index 8adad202..5a0d5afe 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/PreferencesImpl.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/PreferencesImpl.kt @@ -39,6 +39,9 @@ import com.pyamsoft.tetherfi.server.TweakPreferences import com.pyamsoft.tetherfi.server.WifiPreferences import com.pyamsoft.tetherfi.server.broadcast.BroadcastType import com.pyamsoft.tetherfi.server.network.PreferredNetwork +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.random.Random import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -48,9 +51,6 @@ import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.random.Random @Singleton internal class PreferencesImpl diff --git a/app/src/main/java/com/pyamsoft/tetherfi/info/InfoEntry.kt b/app/src/main/java/com/pyamsoft/tetherfi/info/InfoEntry.kt index 39e3bc65..30e96290 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/info/InfoEntry.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/info/InfoEntry.kt @@ -23,6 +23,7 @@ import com.pyamsoft.pydroid.ui.inject.ComposableInjector import com.pyamsoft.pydroid.ui.inject.rememberComposableInjector import com.pyamsoft.pydroid.ui.util.rememberNotNull import com.pyamsoft.tetherfi.ObjectGraph +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.ui.ServerViewState import javax.inject.Inject @@ -43,6 +44,7 @@ internal class InfoInjector : ComposableInjector() { fun InfoEntry( modifier: Modifier = Modifier, appName: String, + featureFlags: FeatureFlags, serverViewState: ServerViewState, onShowQRCode: () -> Unit, onShowSlowSpeedHelp: () -> Unit, @@ -54,6 +56,7 @@ fun InfoEntry( modifier = modifier, state = viewModel, appName = appName, + featureFlags = featureFlags, serverViewState = serverViewState, onShowQRCode = onShowQRCode, onShowSlowSpeedHelp = onShowSlowSpeedHelp, diff --git a/app/src/main/java/com/pyamsoft/tetherfi/main/MainContent.kt b/app/src/main/java/com/pyamsoft/tetherfi/main/MainContent.kt index f472e661..9d0eab23 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/main/MainContent.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/main/MainContent.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.pyamsoft.tetherfi.connections.ConnectionEntry +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.info.InfoEntry import com.pyamsoft.tetherfi.server.status.RunningStatus import com.pyamsoft.tetherfi.status.StatusEntry @@ -32,6 +33,7 @@ import com.pyamsoft.tetherfi.ui.ServerViewState fun MainContent( modifier: Modifier = Modifier, appName: String, + featureFlags: FeatureFlags, pagerState: PagerState, state: ServerViewState, allTabs: List, @@ -70,6 +72,7 @@ fun MainContent( InfoEntry( modifier = Modifier.fillMaxSize(), appName = appName, + featureFlags = featureFlags, serverViewState = state, onShowQRCode = onShowQRCode, onShowSlowSpeedHelp = onShowSlowSpeedHelp, @@ -78,6 +81,7 @@ fun MainContent( MainView.STATUS -> { StatusEntry( modifier = Modifier.fillMaxSize(), + featureFlags = featureFlags, appName = appName, serverViewState = state, onShowQRCode = onShowQRCode, diff --git a/app/src/main/java/com/pyamsoft/tetherfi/main/MainEntry.kt b/app/src/main/java/com/pyamsoft/tetherfi/main/MainEntry.kt index 3cb3612b..5dfe03e1 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/main/MainEntry.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/main/MainEntry.kt @@ -44,6 +44,7 @@ import com.pyamsoft.pydroid.ui.util.fillUpToPortraitSize import com.pyamsoft.pydroid.ui.util.rememberNotNull import com.pyamsoft.tetherfi.ObjectGraph import com.pyamsoft.tetherfi.core.AppDevEnvironment +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.core.Timber import com.pyamsoft.tetherfi.qr.QRCodeEntry import com.pyamsoft.tetherfi.server.broadcast.BroadcastNetworkStatus @@ -68,10 +69,9 @@ internal class MainInjector @Inject internal constructor() : ComposableInjector( @JvmField @Inject internal var viewModel: MainViewModeler? = null @JvmField @Inject internal var appEnvironment: AppDevEnvironment? = null - @JvmField @Inject internal var permissionRequestBus: EventBus? = null - @JvmField @Inject internal var permissionResponseBus: EventConsumer? = null + @JvmField @Inject internal var featureFlags: FeatureFlags? = null override fun onInject(activity: ComponentActivity) { ObjectGraph.ActivityScope.retrieve(activity).inject(this) @@ -82,6 +82,7 @@ internal class MainInjector @Inject internal constructor() : ComposableInjector( appEnvironment = null permissionRequestBus = null permissionResponseBus = null + featureFlags = null } } @@ -206,6 +207,7 @@ fun MainEntry( val appEnvironment = rememberNotNull(component.appEnvironment) val permissionRequestBus = rememberNotNull(component.permissionRequestBus) val permissionResponseBus = rememberNotNull(component.permissionResponseBus) + val featureFlags = rememberNotNull(component.featureFlags) // Use the LifecycleOwner.CoroutineScope (Activity usually) // so that the scope does not die because of navigation events @@ -241,6 +243,7 @@ fun MainEntry( modifier = modifier, appName = appName, state = viewModel, + featureFlags = featureFlags, pagerState = pagerState, allTabs = allTabs, onTabChanged = { handleTabSelected(it) }, diff --git a/app/src/main/java/com/pyamsoft/tetherfi/main/MainScreen.kt b/app/src/main/java/com/pyamsoft/tetherfi/main/MainScreen.kt index f598d219..27d8ed25 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/main/MainScreen.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/main/MainScreen.kt @@ -28,11 +28,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.pyamsoft.pydroid.ui.util.rememberAsStateList +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.broadcast.BroadcastNetworkStatus import com.pyamsoft.tetherfi.server.broadcast.BroadcastType import com.pyamsoft.tetherfi.server.network.PreferredNetwork import com.pyamsoft.tetherfi.server.status.RunningStatus import com.pyamsoft.tetherfi.service.prereq.HotspotStartBlocker +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.annotations.TestOnly @@ -41,6 +43,7 @@ fun MainScreen( modifier: Modifier = Modifier, appName: String, state: MainViewState, + featureFlags: FeatureFlags, pagerState: PagerState, allTabs: List, @@ -88,6 +91,7 @@ fun MainScreen( .heightIn( min = remember(pv) { pv.calculateBottomPadding() }, ), + featureFlags = featureFlags, appName = appName, pagerState = pagerState, state = state, @@ -122,7 +126,9 @@ private fun PreviewMainScreen( override val isShowingSlowSpeedHelp = MutableStateFlow(isShowingSlowSpeedHelp) override val group = MutableStateFlow(BroadcastNetworkStatus.GroupInfo.Empty) override val connection = MutableStateFlow(BroadcastNetworkStatus.ConnectionInfo.Empty) - override val port = MutableStateFlow(0) + + override val httpPort = MutableStateFlow(0) + override val socksPort = MutableStateFlow(0) // TODO support RNDIS override val broadcastType = MutableStateFlow(BroadcastType.WIFI_DIRECT) @@ -143,6 +149,7 @@ private fun PreviewMainScreen( val allTabs = MainView.entries.rememberAsStateList() MainScreen( + featureFlags = makeTestFeatureFlags(), appName = "TEST", state = state, pagerState = rememberPagerState { allTabs.size }, diff --git a/app/src/main/java/com/pyamsoft/tetherfi/status/StatusEntry.kt b/app/src/main/java/com/pyamsoft/tetherfi/status/StatusEntry.kt index d0b350b8..99694db2 100644 --- a/app/src/main/java/com/pyamsoft/tetherfi/status/StatusEntry.kt +++ b/app/src/main/java/com/pyamsoft/tetherfi/status/StatusEntry.kt @@ -35,6 +35,7 @@ import com.pyamsoft.pydroid.ui.inject.ComposableInjector import com.pyamsoft.pydroid.ui.inject.rememberComposableInjector import com.pyamsoft.pydroid.ui.util.rememberNotNull import com.pyamsoft.tetherfi.ObjectGraph +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.status.RunningStatus import com.pyamsoft.tetherfi.ui.ServerViewState import javax.inject.Inject @@ -140,6 +141,7 @@ fun StatusEntry( modifier: Modifier = Modifier, appName: String, serverViewState: ServerViewState, + featureFlags: FeatureFlags, // Actions onShowQRCode: () -> Unit, @@ -186,6 +188,7 @@ fun StatusEntry( state = viewModel, serverViewState = serverViewState, appName = appName, + featureFlags = featureFlags, onShowQRCode = onShowQRCode, onRefreshConnection = onRefreshConnection, onJumpToHowTo = onJumpToHowTo, @@ -196,7 +199,8 @@ fun StatusEntry( }, onSsidChanged = { viewModel.handleSsidChanged(it.trim()) }, onPasswordChanged = { viewModel.handlePasswordChanged(it) }, - onPortChanged = { viewModel.handlePortChanged(it) }, + onHttpPortChanged = { viewModel.handlePortChanged(it, ServerPortTypes.HTTP) }, + onSocksPortChanged = { viewModel.handlePortChanged(it, ServerPortTypes.SOCKS) }, onViewSlowSpeedHelp = onShowSlowSpeedHelp, onOpenBatterySettings = { onLaunchIntent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) diff --git a/info/src/main/java/com/pyamsoft/tetherfi/info/InfoScreen.kt b/info/src/main/java/com/pyamsoft/tetherfi/info/InfoScreen.kt index 07e27ee2..14d82925 100644 --- a/info/src/main/java/com/pyamsoft/tetherfi/info/InfoScreen.kt +++ b/info/src/main/java/com/pyamsoft/tetherfi/info/InfoScreen.kt @@ -29,11 +29,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.pyamsoft.pydroid.theme.keylines +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.ui.LANDSCAPE_MAX_WIDTH import com.pyamsoft.tetherfi.ui.ServerViewState import com.pyamsoft.tetherfi.ui.renderLinks import com.pyamsoft.tetherfi.ui.renderPYDroidExtras import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState private enum class InfoContentTypes { @@ -45,6 +47,7 @@ private enum class InfoContentTypes { fun InfoScreen( modifier: Modifier = Modifier, appName: String, + featureFlags: FeatureFlags, state: InfoViewState, serverViewState: ServerViewState, onTogglePasswordVisibility: () -> Unit, @@ -79,6 +82,7 @@ fun InfoScreen( itemModifier = Modifier.widthIn(max = LANDSCAPE_MAX_WIDTH), appName = appName, state = state, + featureFlags = featureFlags, serverViewState = serverViewState, onTogglePasswordVisibility = onTogglePasswordVisibility, onShowQRCode = onShowQRCode, @@ -100,6 +104,7 @@ fun InfoScreen( private fun PreviewInfoScreen() { InfoScreen( appName = "TEST", + featureFlags = makeTestFeatureFlags(), state = MutableInfoViewState(), serverViewState = makeTestServerState(TestServerState.EMPTY), onTogglePasswordVisibility = {}, diff --git a/info/src/main/java/com/pyamsoft/tetherfi/info/RenderConnectionInstructions.kt b/info/src/main/java/com/pyamsoft/tetherfi/info/RenderConnectionInstructions.kt index b067da48..940c7652 100644 --- a/info/src/main/java/com/pyamsoft/tetherfi/info/RenderConnectionInstructions.kt +++ b/info/src/main/java/com/pyamsoft/tetherfi/info/RenderConnectionInstructions.kt @@ -25,12 +25,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.pyamsoft.pydroid.theme.keylines +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.info.sections.renderAppSetup import com.pyamsoft.tetherfi.info.sections.renderConnectionComplete import com.pyamsoft.tetherfi.info.sections.renderDeviceIdentifiers import com.pyamsoft.tetherfi.info.sections.renderDeviceSetup import com.pyamsoft.tetherfi.ui.ServerViewState import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import org.jetbrains.annotations.TestOnly @@ -41,6 +43,7 @@ private enum class ConnectionInstructionContentTypes { internal fun LazyListScope.renderConnectionInstructions( itemModifier: Modifier = Modifier, appName: String, + featureFlags: FeatureFlags, state: InfoViewState, serverViewState: ServerViewState, onShowQRCode: () -> Unit, @@ -85,6 +88,7 @@ internal fun LazyListScope.renderConnectionInstructions( itemModifier = itemModifier, appName = appName, state = state, + featureFlags = featureFlags, serverViewState = serverViewState, onTogglePasswordVisibility = onTogglePasswordVisibility, onShowQRCode = onShowQRCode, @@ -119,6 +123,7 @@ private fun PreviewConnectionInstructions(state: InfoViewState, server: TestServ LazyColumn { renderConnectionInstructions( appName = "TEST", + featureFlags = makeTestFeatureFlags(), serverViewState = makeTestServerState(server), state = state, onTogglePasswordVisibility = {}, diff --git a/info/src/main/java/com/pyamsoft/tetherfi/info/sections/RenderDeviceSetup.kt b/info/src/main/java/com/pyamsoft/tetherfi/info/sections/RenderDeviceSetup.kt index 917c5bbf..ddb115a7 100644 --- a/info/src/main/java/com/pyamsoft/tetherfi/info/sections/RenderDeviceSetup.kt +++ b/info/src/main/java/com/pyamsoft/tetherfi/info/sections/RenderDeviceSetup.kt @@ -16,6 +16,7 @@ package com.pyamsoft.tetherfi.info.sections +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,15 +26,17 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -42,6 +45,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pyamsoft.pydroid.theme.keylines import com.pyamsoft.pydroid.ui.haptics.LocalHapticManager +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.info.InfoViewState import com.pyamsoft.tetherfi.info.MutableInfoViewState import com.pyamsoft.tetherfi.info.R @@ -55,8 +59,10 @@ import com.pyamsoft.tetherfi.ui.rememberServerPassword import com.pyamsoft.tetherfi.ui.rememberServerRawPassword import com.pyamsoft.tetherfi.ui.rememberServerSSID import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import org.jetbrains.annotations.TestOnly +import rememberPortNumber private enum class DeviceSetupContentTypes { SETTINGS, @@ -67,6 +73,7 @@ internal fun LazyListScope.renderDeviceSetup( itemModifier: Modifier = Modifier, appName: String, state: InfoViewState, + featureFlags: FeatureFlags, serverViewState: ServerViewState, onShowQRCode: () -> Unit, onTogglePasswordVisibility: () -> Unit, @@ -231,72 +238,192 @@ internal fun LazyListScope.renderDeviceSetup( style = MaterialTheme.typography.bodyLarge, ) - Text( - modifier = Modifier.padding(top = MaterialTheme.keylines.baseline), - text = stringResource(R.string.http_manual_proxy), - style = - MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) + // TODO(Peter): Move into VM scope + val (showHttpOptions, setShowHttpOptions) = remember { mutableStateOf(false) } + val (showSocksOptions, setShowSocksOptions) = remember { mutableStateOf(false) } - Row( - modifier = Modifier.padding(top = MaterialTheme.keylines.typography), - verticalAlignment = Alignment.CenterVertically, + Column( + modifier = Modifier.padding(top = MaterialTheme.keylines.baseline), ) { - val connection by serverViewState.connection.collectAsStateWithLifecycle() - val ipAddress = rememberServerHostname(connection) + if (featureFlags.isSocksProxyEnabled) { + Row( + modifier = Modifier.clickable { setShowHttpOptions(!showHttpOptions) }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.view_http_options), + style = MaterialTheme.typography.labelLarge, + ) - Text( - text = stringResource(R.string.label_hotspot_hostname), - style = - MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) - Text( - modifier = Modifier.padding(start = MaterialTheme.keylines.typography), - text = ipAddress, - style = - MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.W700, - fontFamily = FontFamily.Monospace, - ), - ) + Icon( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + imageVector = + if (showHttpOptions) Icons.AutoMirrored.Filled.KeyboardArrowRight + else Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.view_http_options), + ) + } + } + + AnimatedVisibility( + visible = showHttpOptions || !featureFlags.isSocksProxyEnabled, + ) { + Column( + modifier = Modifier.padding(top = MaterialTheme.keylines.baseline), + ) { + Text( + text = stringResource(R.string.http_manual_proxy), + style = + MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + + Row( + modifier = Modifier.padding(top = MaterialTheme.keylines.typography), + verticalAlignment = Alignment.CenterVertically, + ) { + val connection by serverViewState.connection.collectAsStateWithLifecycle() + val ipAddress = rememberServerHostname(connection) + + Text( + text = stringResource(R.string.label_hotspot_hostname), + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + Text( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + text = ipAddress, + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.W700, + fontFamily = FontFamily.Monospace, + ), + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + val httpPortNumber by serverViewState.httpPort.collectAsStateWithLifecycle() + val httpPort = rememberPortNumber(httpPortNumber) + + Text( + text = stringResource(R.string.label_hotspot_http_port), + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + + Text( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + text = httpPort, + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.W700, + fontFamily = FontFamily.Monospace, + ), + ) + } + } + } } - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.label_hotspot_port), - style = - MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) + if (featureFlags.isSocksProxyEnabled) { + Column( + modifier = Modifier.padding(top = MaterialTheme.keylines.baseline), + ) { + Row( + modifier = Modifier.clickable { setShowSocksOptions(!showSocksOptions) }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.view_socks_options), + style = MaterialTheme.typography.labelLarge, + ) + + Icon( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + imageVector = + if (showSocksOptions) Icons.AutoMirrored.Filled.KeyboardArrowRight + else Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.view_http_options), + ) + } - val context = LocalContext.current - val port by serverViewState.port.collectAsStateWithLifecycle() - val portNumber = - remember( - context, - port, + AnimatedVisibility( + visible = showSocksOptions, + ) { + Column( + modifier = Modifier.padding(top = MaterialTheme.keylines.content), ) { - if (port <= 1024) context.getString(com.pyamsoft.tetherfi.ui.R.string.invalid_port) - else "$port" + Text( + text = stringResource(R.string.socks_manual_proxy), + style = + MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + + Row( + modifier = Modifier.padding(top = MaterialTheme.keylines.typography), + verticalAlignment = Alignment.CenterVertically, + ) { + val connection by serverViewState.connection.collectAsStateWithLifecycle() + val ipAddress = rememberServerHostname(connection) + + Text( + text = stringResource(R.string.label_hotspot_hostname), + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + Text( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + text = ipAddress, + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.W700, + fontFamily = FontFamily.Monospace, + ), + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + val socksPortNumber by serverViewState.socksPort.collectAsStateWithLifecycle() + val socksPort = rememberPortNumber(socksPortNumber) + + Text( + text = stringResource(R.string.label_hotspot_socks_port), + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + + Text( + modifier = Modifier.padding(start = MaterialTheme.keylines.typography), + text = socksPort, + style = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.W700, + fontFamily = FontFamily.Monospace, + ), + ) + } } - Text( - modifier = Modifier.padding(start = MaterialTheme.keylines.typography), - text = portNumber, - style = - MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.W700, - fontFamily = FontFamily.Monospace, - ), - ) + } + } } } } @@ -309,6 +436,7 @@ private fun PreviewDeviceSetup(state: InfoViewState, server: TestServerState) { LazyColumn { renderDeviceSetup( appName = "TEST", + featureFlags = makeTestFeatureFlags(), serverViewState = makeTestServerState(server), state = state, onTogglePasswordVisibility = {}, diff --git a/info/src/main/res/values-zh-rTW/devivce_setup_strings.xml b/info/src/main/res/values-zh-rTW/devivce_setup_strings.xml index 0bf46c68..5ba57f54 100644 --- a/info/src/main/res/values-zh-rTW/devivce_setup_strings.xml +++ b/info/src/main/res/values-zh-rTW/devivce_setup_strings.xml @@ -24,7 +24,6 @@ 打開 Wi-Fi 連線設定頁面 名稱 網址(URL) - 通訊埠號 Configure the proxy settings - Use MANUAL mode and configure both HTTP and HTTPS proxy options. Connect to the %1$s Hotspot Open the Wi-Fi settings page Name URL - Port Open the Connection/Internet settings page + + + Configure HTTP proxy settings + Configure SOCKS settings + + + HTTP Port + SOCKS Port + + Use MANUAL mode and configure both HTTP and HTTPS proxy options. + + + Use MANUAL mode and configure SOCKS proxy options. SOCKS proxy will resolve DNS. diff --git a/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewModeler.kt b/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewModeler.kt index 3f590f2d..d4fad7f7 100644 --- a/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewModeler.kt +++ b/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewModeler.kt @@ -87,7 +87,11 @@ internal constructor( // Port is its own thing, not part of group info proxyPreferences.listenForHttpPortChanges().also { f -> - scope.launch(context = Dispatchers.Default) { f.collect { s.port.value = it } } + scope.launch(context = Dispatchers.Default) { f.collect { s.httpPort.value = it } } + } + + proxyPreferences.listenForSocksPortChanges().also { f -> + scope.launch(context = Dispatchers.Default) { f.collect { s.socksPort.value = it } } } // Broadcast type diff --git a/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewState.kt b/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewState.kt index 0fe5df48..869ea65f 100644 --- a/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewState.kt +++ b/main/src/main/java/com/pyamsoft/tetherfi/main/MainViewState.kt @@ -65,7 +65,10 @@ class MutableMainViewState @Inject internal constructor() : MainViewState { override val connection = MutableStateFlow( BroadcastNetworkStatus.ConnectionInfo.Empty) - override val port = MutableStateFlow(0) + + override val httpPort = MutableStateFlow(0) + override val socksPort = MutableStateFlow(0) + override val broadcastType = MutableStateFlow(null) override val preferredNetwork = MutableStateFlow(null) diff --git a/server/src/main/java/com/pyamsoft/tetherfi/server/proxy/manager/factory/DefaultProxyManagerFactory.kt b/server/src/main/java/com/pyamsoft/tetherfi/server/proxy/manager/factory/DefaultProxyManagerFactory.kt index a369e9e0..d6f79df4 100644 --- a/server/src/main/java/com/pyamsoft/tetherfi/server/proxy/manager/factory/DefaultProxyManagerFactory.kt +++ b/server/src/main/java/com/pyamsoft/tetherfi/server/proxy/manager/factory/DefaultProxyManagerFactory.kt @@ -111,7 +111,7 @@ internal constructor( ): ProxyManager { enforcer.assertOffMainThread() - val port = proxyPreferences.listenForHttpPortChanges().first() + val port = proxyPreferences.listenForSocksPortChanges().first() return createTcp( proxyType = SharedProxy.Type.SOCKS, @@ -119,7 +119,7 @@ internal constructor( info = info, socketCreator = socketCreator, dispatcher = dispatcher, - port = port + 1, + port = port, ) } diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusLoadedContent.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusLoadedContent.kt index 40b37a3b..9c0cac31 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusLoadedContent.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusLoadedContent.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.pyamsoft.pydroid.theme.keylines +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.ServerNetworkBand import com.pyamsoft.tetherfi.server.broadcast.BroadcastType import com.pyamsoft.tetherfi.server.network.PreferredNetwork @@ -44,6 +45,7 @@ import com.pyamsoft.tetherfi.ui.test.TEST_PASSWORD import com.pyamsoft.tetherfi.ui.test.TEST_PORT import com.pyamsoft.tetherfi.ui.test.TEST_SSID import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import org.jetbrains.annotations.TestOnly @@ -56,6 +58,7 @@ internal fun LazyListScope.renderLoadedContent( itemModifier: Modifier = Modifier, appName: String, state: StatusViewState, + featureFlags: FeatureFlags, serverViewState: ServerViewState, isEditable: Boolean, @@ -65,7 +68,8 @@ internal fun LazyListScope.renderLoadedContent( onSsidChanged: (String) -> Unit, onPasswordChanged: (String) -> Unit, onTogglePasswordVisibility: () -> Unit, - onPortChanged: (String) -> Unit, + onHttpPortChanged: (String) -> Unit, + onSocksPortChanged: (String) -> Unit, onSelectBand: (ServerNetworkBand) -> Unit, // Battery @@ -104,12 +108,14 @@ internal fun LazyListScope.renderLoadedContent( isEditable = isEditable, appName = appName, state = state, + featureFlags = featureFlags, serverViewState = serverViewState, wiDiStatus = wiDiStatus, proxyStatus = proxyStatus, onSsidChanged = onSsidChanged, onPasswordChanged = onPasswordChanged, - onPortChanged = onPortChanged, + onHttpPortChanged = onHttpPortChanged, + onSocksPortChanged = onSocksPortChanged, onTogglePasswordVisibility = onTogglePasswordVisibility, onShowQRCode = onShowQRCode, onRefreshConnection = onRefreshConnection, @@ -213,16 +219,18 @@ private fun PreviewLoadedContent( loadingState.value = StatusViewState.LoadingState.DONE this.ssid.value = TEST_SSID this.password.value = TEST_PASSWORD - this.port.value = "$TEST_PORT" + this.httpPort.value = "$TEST_PORT" band.value = ServerNetworkBand.LEGACY }, serverViewState = makeTestServerState(state), + featureFlags = makeTestFeatureFlags(), appName = "TEST", onRequestNotificationPermission = {}, onSelectBand = {}, onOpenBatterySettings = {}, onPasswordChanged = {}, - onPortChanged = {}, + onHttpPortChanged = {}, + onSocksPortChanged = {}, onSsidChanged = {}, onTogglePasswordVisibility = {}, onShowQRCode = {}, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusScreen.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusScreen.kt index 42ec78b0..80135393 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusScreen.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pyamsoft.pydroid.theme.keylines import com.pyamsoft.pydroid.ui.util.fillUpToPortraitSize +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.ServerNetworkBand import com.pyamsoft.tetherfi.server.ServerPerformanceLimit import com.pyamsoft.tetherfi.server.ServerSocketTimeout @@ -47,6 +48,7 @@ import com.pyamsoft.tetherfi.ui.test.TEST_PASSWORD import com.pyamsoft.tetherfi.ui.test.TEST_PORT import com.pyamsoft.tetherfi.ui.test.TEST_SSID import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import org.jetbrains.annotations.TestOnly @@ -65,6 +67,7 @@ fun StatusScreen( modifier: Modifier = Modifier, appName: String, state: StatusViewState, + featureFlags: FeatureFlags, serverViewState: ServerViewState, // Proxy @@ -76,7 +79,8 @@ fun StatusScreen( onPasswordChanged: (String) -> Unit, onTogglePasswordVisibility: () -> Unit, onSelectBand: (ServerNetworkBand) -> Unit, - onPortChanged: (String) -> Unit, + onHttpPortChanged: (String) -> Unit, + onSocksPortChanged: (String) -> Unit, // Battery Optimization onOpenBatterySettings: () -> Unit, @@ -223,6 +227,7 @@ fun StatusScreen( itemModifier = Modifier.width(LANDSCAPE_MAX_WIDTH), appName = appName, state = state, + featureFlags = featureFlags, serverViewState = serverViewState, isEditable = isEditable, wiDiStatus = wiDiStatus, @@ -230,7 +235,8 @@ fun StatusScreen( showNotificationSettings = showNotificationSettings, onSsidChanged = onSsidChanged, onPasswordChanged = onPasswordChanged, - onPortChanged = onPortChanged, + onHttpPortChanged = onHttpPortChanged, + onSocksPortChanged = onSocksPortChanged, onOpenBatterySettings = onOpenBatterySettings, onSelectBand = onSelectBand, onRequestNotificationPermission = onRequestNotificationPermission, @@ -280,17 +286,19 @@ private fun PreviewStatusScreen( else StatusViewState.LoadingState.DONE this.ssid.value = ssid this.password.value = password - this.port.value = "$port" + this.httpPort.value = "$port" band.value = ServerNetworkBand.LEGACY }, serverViewState = makeTestServerState(TestServerState.EMPTY), appName = "TEST", + featureFlags = makeTestFeatureFlags(), onStatusUpdated = {}, onRequestNotificationPermission = {}, onSelectBand = {}, onOpenBatterySettings = {}, onPasswordChanged = {}, - onPortChanged = {}, + onHttpPortChanged = {}, + onSocksPortChanged = {}, onSsidChanged = {}, onToggleProxy = {}, onTogglePasswordVisibility = {}, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewModeler.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewModeler.kt index 69c5aff5..ee173f29 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewModeler.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewModeler.kt @@ -59,7 +59,8 @@ internal constructor( private data class LoadConfig( var ssid: Boolean, var password: Boolean, - var port: Boolean, + var httpPort: Boolean, + var socksPort: Boolean, var band: Boolean, var tweakIgnoreVpn: Boolean, var tweakIgnoreLocation: Boolean, @@ -70,7 +71,8 @@ internal constructor( ) private fun markPreferencesLoaded(config: LoadConfig) { - if (config.port && + if (config.httpPort && + config.socksPort && config.tweakIgnoreVpn && config.tweakShutdownWithNoClients && config.ssid && @@ -125,7 +127,8 @@ internal constructor( LoadConfig( ssid = false, password = false, - port = false, + httpPort = false, + socksPort = false, band = false, tweakIgnoreVpn = false, tweakIgnoreLocation = false, @@ -251,9 +254,19 @@ internal constructor( proxyPreferences.listenForHttpPortChanges().also { f -> scope.launch(context = Dispatchers.Default) { val p = f.first() - s.port.value = if (p == 0) "" else "$p" + s.httpPort.value = if (p == 0) "" else "$p" - config.port = true + config.httpPort = true + markPreferencesLoaded(config) + } + } + + proxyPreferences.listenForSocksPortChanges().also { f -> + scope.launch(context = Dispatchers.Default) { + val p = f.first() + s.socksPort.value = if (p == 0) "" else "$p" + + config.socksPort = true markPreferencesLoaded(config) } } @@ -350,12 +363,19 @@ internal constructor( wifiPreferences.setPassword(password) } - fun handlePortChanged(port: String) { - state.port.value = port - - val portValue = port.toIntOrNull() - proxyPreferences.setHttpPort(portValue ?: 0) - } + fun handlePortChanged(port: String, type: ServerPortTypes) = + when (type) { + ServerPortTypes.HTTP -> { + state.httpPort.value = port + val portValue = port.toIntOrNull() + proxyPreferences.setHttpPort(portValue ?: 0) + } + ServerPortTypes.SOCKS -> { + state.socksPort.value = port + val portValue = port.toIntOrNull() + proxyPreferences.setSocksPort(portValue ?: 0) + } + } fun handleChangeBand(band: ServerNetworkBand) { state.band.value = band diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewState.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewState.kt index 8be5d388..5efc9514 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewState.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/StatusViewState.kt @@ -31,6 +31,11 @@ enum class StatusViewDialogs { SOCKET_TIMEOUT } +enum class ServerPortTypes { + HTTP, + SOCKS +} + enum class StatusViewTweaks { IGNORE_VPN, IGNORE_LOCATION, @@ -47,7 +52,8 @@ interface StatusViewState : UiViewState { val ssid: StateFlow val password: StateFlow val isPasswordVisible: StateFlow - val port: StateFlow + val httpPort: StateFlow + val socksPort: StateFlow val band: StateFlow // Operating Settings @@ -84,7 +90,8 @@ class MutableStatusViewState @Inject internal constructor() : StatusViewState { override val ssid = MutableStateFlow("") override val password = MutableStateFlow("") override val isPasswordVisible = MutableStateFlow(false) - override val port = MutableStateFlow("") + override val httpPort = MutableStateFlow("") + override val socksPort = MutableStateFlow("") override val band = MutableStateFlow(null) override val hasNotificationPermission = MutableStateFlow(false) diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/EditModeWidgets.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/EditModeWidgets.kt index 6bb2dfdf..c2399a32 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/EditModeWidgets.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/EditModeWidgets.kt @@ -16,6 +16,7 @@ package com.pyamsoft.tetherfi.status.sections.network +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions @@ -48,18 +49,18 @@ import com.pyamsoft.tetherfi.ui.icons.Visibility import com.pyamsoft.tetherfi.ui.icons.VisibilityOff @Composable -internal fun EditPort( +private fun EditPort( modifier: Modifier = Modifier, - state: StatusViewState, + port: String, + @StringRes portLabelRes: Int, onPortChanged: (String) -> Unit, ) { - val port by state.port.collectAsStateWithLifecycle() val portNumber = remember(port) { port.toIntOrNull() } val isValid = remember(portNumber) { portNumber != null && portNumber in 1025..65000 } StatusEditor( modifier = modifier, - title = stringResource(R.string.editmode_hotspot_port), + title = stringResource(portLabelRes), value = port, onChange = onPortChanged, keyboardOptions = @@ -72,15 +73,46 @@ internal fun EditPort( description = stringResource( R.string.editmode_label_map, - stringResource(R.string.editmode_type_port), + stringResource(portLabelRes), stringResource( if (isValid) R.string.editmode_label_valid else R.string.editmode_label_invalid), - )) + ), + ) }, ) } +@Composable +internal fun EditHttpPort( + modifier: Modifier = Modifier, + state: StatusViewState, + onPortChanged: (String) -> Unit, +) { + val port by state.httpPort.collectAsStateWithLifecycle() + EditPort( + modifier = modifier, + port = port, + portLabelRes = R.string.editmode_type_http_port, + onPortChanged = onPortChanged, + ) +} + +@Composable +internal fun EditSocksPort( + modifier: Modifier = Modifier, + state: StatusViewState, + onPortChanged: (String) -> Unit, +) { + val port by state.socksPort.collectAsStateWithLifecycle() + EditPort( + modifier = modifier, + port = port, + portLabelRes = R.string.editmode_type_socks_port, + onPortChanged = onPortChanged, + ) +} + @Composable internal fun EditPassword( modifier: Modifier = Modifier, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/NetworkInformation.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/NetworkInformation.kt index b3a13b44..19382368 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/NetworkInformation.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/NetworkInformation.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pyamsoft.pydroid.theme.keylines import com.pyamsoft.pydroid.ui.theme.HairlineSize +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.status.RunningStatus import com.pyamsoft.tetherfi.status.MutableStatusViewState import com.pyamsoft.tetherfi.status.StatusViewState @@ -42,6 +43,7 @@ import com.pyamsoft.tetherfi.ui.test.TEST_PASSWORD import com.pyamsoft.tetherfi.ui.test.TEST_PORT import com.pyamsoft.tetherfi.ui.test.TEST_SSID import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import com.pyamsoft.tetherfi.ui.trouble.TroubleshootUnableToStart import org.jetbrains.annotations.TestOnly @@ -53,6 +55,7 @@ private enum class NetworkStatusWidgetsContentTypes { internal fun LazyListScope.renderNetworkInformation( itemModifier: Modifier = Modifier, appName: String, + featureFlags: FeatureFlags, // State state: StatusViewState, @@ -66,7 +69,8 @@ internal fun LazyListScope.renderNetworkInformation( // Network config onSsidChanged: (String) -> Unit, onPasswordChanged: (String) -> Unit, - onPortChanged: (String) -> Unit, + onHttpPortChanged: (String) -> Unit, + onSocksPortChanged: (String) -> Unit, onTogglePasswordVisibility: () -> Unit, // Connections @@ -124,10 +128,12 @@ internal fun LazyListScope.renderNetworkInformation( renderEditableItems( modifier = itemModifier, state = state, + featureFlags = featureFlags, serverViewState = serverViewState, onSsidChanged = onSsidChanged, onPasswordChanged = onPasswordChanged, - onPortChanged = onPortChanged, + onHttpPortChanged = onHttpPortChanged, + onSocksPortChanged = onSocksPortChanged, onTogglePasswordVisibility = onTogglePasswordVisibility, ) } else { @@ -160,6 +166,7 @@ private fun PreviewNetworkInformation( LazyColumn { renderNetworkInformation( itemModifier = Modifier.width(LANDSCAPE_MAX_WIDTH), + featureFlags = makeTestFeatureFlags(), wiDiStatus = wiDiStatus, proxyStatus = proxyStatus, appName = "TEST", @@ -169,13 +176,14 @@ private fun PreviewNetworkInformation( MutableStatusViewState().apply { this.ssid.value = ssid this.password.value = password - this.port.value = port + this.httpPort.value = port }, onShowNetworkError = {}, onShowQRCode = {}, onRefreshConnection = {}, onJumpToHowTo = {}, - onPortChanged = {}, + onHttpPortChanged = {}, + onSocksPortChanged = {}, onSsidChanged = {}, onPasswordChanged = {}, onShowHotspotError = {}, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderEditableItems.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderEditableItems.kt index 1c153630..b09fd06e 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderEditableItems.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderEditableItems.kt @@ -16,6 +16,7 @@ package com.pyamsoft.tetherfi.status.sections.network +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pyamsoft.pydroid.theme.keylines +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.broadcast.BroadcastType import com.pyamsoft.tetherfi.status.MutableStatusViewState import com.pyamsoft.tetherfi.status.StatusViewState @@ -36,6 +38,7 @@ import com.pyamsoft.tetherfi.ui.test.TEST_PASSWORD import com.pyamsoft.tetherfi.ui.test.TEST_PORT import com.pyamsoft.tetherfi.ui.test.TEST_SSID import com.pyamsoft.tetherfi.ui.test.TestServerState +import com.pyamsoft.tetherfi.ui.test.makeTestFeatureFlags import com.pyamsoft.tetherfi.ui.test.makeTestServerState import org.jetbrains.annotations.TestOnly @@ -47,11 +50,13 @@ private enum class RenderEditableItemsContentTypes { internal fun LazyListScope.renderEditableItems( modifier: Modifier = Modifier, + featureFlags: FeatureFlags, state: StatusViewState, serverViewState: ServerViewState, onSsidChanged: (String) -> Unit, onPasswordChanged: (String) -> Unit, - onPortChanged: (String) -> Unit, + onHttpPortChanged: (String) -> Unit, + onSocksPortChanged: (String) -> Unit, onTogglePasswordVisibility: () -> Unit, ) { item( @@ -86,11 +91,30 @@ internal fun LazyListScope.renderEditableItems( item( contentType = RenderEditableItemsContentTypes.EDIT_PORT, ) { - EditPort( + Row( modifier = modifier.padding(bottom = MaterialTheme.keylines.baseline), - state = state, - onPortChanged = onPortChanged, - ) + ) { + EditHttpPort( + modifier = + Modifier.weight(1F).run { + if (featureFlags.isSocksProxyEnabled) { + padding(end = MaterialTheme.keylines.content) + } else { + this + } + }, + state = state, + onPortChanged = onHttpPortChanged, + ) + + if (featureFlags.isSocksProxyEnabled) { + EditSocksPort( + modifier = Modifier.weight(1F), + state = state, + onPortChanged = onSocksPortChanged, + ) + } + } } } @@ -108,9 +132,11 @@ private fun PreviewEditableItems( MutableStatusViewState().apply { this.ssid.value = ssid this.password.value = password - this.port.value = port + this.httpPort.value = port }, - onPortChanged = {}, + featureFlags = makeTestFeatureFlags(), + onHttpPortChanged = {}, + onSocksPortChanged = {}, onSsidChanged = {}, onPasswordChanged = {}, onTogglePasswordVisibility = {}, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderRunningItems.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderRunningItems.kt index 75d83ff1..636cfdc7 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderRunningItems.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/RenderRunningItems.kt @@ -134,7 +134,7 @@ private fun PreviewRunningItems( MutableStatusViewState().apply { this.ssid.value = TEST_SSID this.password.value = TEST_PASSWORD - this.port.value = "$TEST_PORT" + this.httpPort.value = "$TEST_PORT" }, onShowNetworkError = {}, onShowQRCode = {}, diff --git a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/ViewModeWidgets.kt b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/ViewModeWidgets.kt index 23102134..eaa77e57 100644 --- a/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/ViewModeWidgets.kt +++ b/status/src/main/java/com/pyamsoft/tetherfi/status/sections/network/ViewModeWidgets.kt @@ -19,9 +19,7 @@ package com.pyamsoft.tetherfi.status.sections.network import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton @@ -33,7 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation @@ -57,31 +54,26 @@ import com.pyamsoft.tetherfi.ui.icons.VisibilityOff import com.pyamsoft.tetherfi.ui.rememberServerHostname import com.pyamsoft.tetherfi.ui.rememberServerPassword import com.pyamsoft.tetherfi.ui.rememberServerSSID +import rememberPortNumber @Composable internal fun ViewProxy( modifier: Modifier = Modifier, serverViewState: ServerViewState, ) { - val context = LocalContext.current val connection by serverViewState.connection.collectAsStateWithLifecycle() val ipAddress = rememberServerHostname(connection) - val portNumber by serverViewState.port.collectAsStateWithLifecycle() - val port = - remember( - context, - portNumber, - ) { - if (portNumber in 1024..65000) "$portNumber" else context.getString(R2.string.invalid_port) - } + val httpPortNumber by serverViewState.httpPort.collectAsStateWithLifecycle() + val socksPortNumber by serverViewState.socksPort.collectAsStateWithLifecycle() + val httpPort = rememberPortNumber(httpPortNumber) + val socksPort = rememberPortNumber(socksPortNumber) - Row( + Column( modifier = modifier, - verticalAlignment = Alignment.CenterVertically, ) { StatusItem( - modifier = Modifier.weight(1F, fill = false), + modifier = Modifier.padding(vertical = MaterialTheme.keylines.baseline), title = stringResource(R.string.viewmode_hotspot_hostname), value = ipAddress, valueStyle = @@ -91,19 +83,30 @@ internal fun ViewProxy( ), ) - Spacer( - modifier = Modifier.width(MaterialTheme.keylines.content), - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + StatusItem( + modifier = Modifier.padding(end = MaterialTheme.keylines.content), + title = stringResource(R.string.viewmode_hotspot_http_port), + value = httpPort, + valueStyle = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.W400, + fontFamily = FontFamily.Monospace, + ), + ) - StatusItem( - title = stringResource(R.string.viewmode_hotspot_port), - value = port, - valueStyle = - MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.W400, - fontFamily = FontFamily.Monospace, - ), - ) + StatusItem( + title = stringResource(R.string.viewmode_hotspot_socks_port), + value = socksPort, + valueStyle = + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.W400, + fontFamily = FontFamily.Monospace, + ), + ) + } } } diff --git a/status/src/main/res/values-zh-rTW/edit_mode_strings.xml b/status/src/main/res/values-zh-rTW/edit_mode_strings.xml index 3d75c4a5..2bbfb2a7 100644 --- a/status/src/main/res/values-zh-rTW/edit_mode_strings.xml +++ b/status/src/main/res/values-zh-rTW/edit_mode_strings.xml @@ -16,10 +16,8 @@ ~ limitations under the License. --> - 代理服務通訊埠號 由作業系統設定:無法變更 - 通訊埠號 網路名稱(SSID) %1$s是%2$s diff --git a/status/src/main/res/values-zh-rTW/view_mode_strings.xml b/status/src/main/res/values-zh-rTW/view_mode_strings.xml index 294728ac..fb2bc909 100644 --- a/status/src/main/res/values-zh-rTW/view_mode_strings.xml +++ b/status/src/main/res/values-zh-rTW/view_mode_strings.xml @@ -22,6 +22,5 @@ >不知道下一步要做什麼嗎?\n查看%1$s 網路熱點名稱 網路熱點密碼 - 代理服務的通訊埠號 代理服務的網址/主機名稱 diff --git a/status/src/main/res/values/edit_mode_strings.xml b/status/src/main/res/values/edit_mode_strings.xml index c94ffa9c..0010392c 100644 --- a/status/src/main/res/values/edit_mode_strings.xml +++ b/status/src/main/res/values/edit_mode_strings.xml @@ -15,17 +15,20 @@ ~ limitations under the License. --> - PROXY PORT SET BY SYSTEM: CAN\'T CHANGE - Port SSID %1$s is %2$s + Valid Invalid Visible Hidden HOTSPOT PASSWORD HOTSPOT NAME + + + HTTP PORT + SOCKS PORT diff --git a/status/src/main/res/values/view_mode_strings.xml b/status/src/main/res/values/view_mode_strings.xml index 447cd3bf..5ce4ce9f 100644 --- a/status/src/main/res/values/view_mode_strings.xml +++ b/status/src/main/res/values/view_mode_strings.xml @@ -21,6 +21,9 @@ >Confused on what to do next?\nView the %1$s HOTSPOT NAME HOTSPOT PASSWORD - PROXY PORT PROXY URL/HOSTNAME + + + HTTP PROXY PORT + SOCKS PROXY PORT diff --git a/ui/src/main/java/PortFormatter.kt b/ui/src/main/java/PortFormatter.kt new file mode 100644 index 00000000..ed57e14e --- /dev/null +++ b/ui/src/main/java/PortFormatter.kt @@ -0,0 +1,33 @@ +import androidx.annotation.CheckResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.pyamsoft.tetherfi.ui.R + +/* + * Copyright 2024 pyamsoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@CheckResult +@Composable +fun rememberPortNumber(portNumber: Int): String { + val context = LocalContext.current + return remember( + context, + portNumber, + ) { + if (portNumber in 1024..65000) "$portNumber" else context.getString(R.string.invalid_port) + } +} diff --git a/ui/src/main/java/com/pyamsoft/tetherfi/ui/ServerViewState.kt b/ui/src/main/java/com/pyamsoft/tetherfi/ui/ServerViewState.kt index 74aa9943..59722feb 100644 --- a/ui/src/main/java/com/pyamsoft/tetherfi/ui/ServerViewState.kt +++ b/ui/src/main/java/com/pyamsoft/tetherfi/ui/ServerViewState.kt @@ -30,7 +30,8 @@ interface ServerViewState : UiViewState { val group: StateFlow val connection: StateFlow - val port: StateFlow + val httpPort: StateFlow + val socksPort: StateFlow val broadcastType: StateFlow val preferredNetwork: StateFlow diff --git a/ui/src/main/java/com/pyamsoft/tetherfi/ui/test/TestServerViewState.kt b/ui/src/main/java/com/pyamsoft/tetherfi/ui/test/TestServerViewState.kt index 9bc9c71f..7244484a 100644 --- a/ui/src/main/java/com/pyamsoft/tetherfi/ui/test/TestServerViewState.kt +++ b/ui/src/main/java/com/pyamsoft/tetherfi/ui/test/TestServerViewState.kt @@ -18,6 +18,7 @@ package com.pyamsoft.tetherfi.ui.test import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting +import com.pyamsoft.tetherfi.core.FeatureFlags import com.pyamsoft.tetherfi.server.broadcast.BroadcastNetworkStatus import com.pyamsoft.tetherfi.server.broadcast.BroadcastType import com.pyamsoft.tetherfi.server.network.PreferredNetwork @@ -46,6 +47,17 @@ enum class TestServerState { @VisibleForTesting val TEST_CLIENT_LIST: Collection = emptySet() +@TestOnly +@CheckResult +@VisibleForTesting +fun makeTestFeatureFlags( + isSocksProxyEnabled: Boolean = false, +): FeatureFlags { + return object : FeatureFlags { + override val isSocksProxyEnabled = isSocksProxyEnabled + } +} + @TestOnly @CheckResult @VisibleForTesting @@ -58,7 +70,8 @@ fun makeTestServerState( object : ServerViewState { override val group = MutableStateFlow(BroadcastNetworkStatus.GroupInfo.Empty) override val connection = MutableStateFlow(BroadcastNetworkStatus.ConnectionInfo.Empty) - override val port = MutableStateFlow(TEST_PORT) + override val httpPort = MutableStateFlow(TEST_PORT) + override val socksPort = MutableStateFlow(TEST_PORT + 1) override val broadcastType = MutableStateFlow(broadcastType) @@ -81,7 +94,8 @@ fun makeTestServerState( override val connection = MutableStateFlow( BroadcastNetworkStatus.ConnectionInfo.Connected(hostName = TEST_HOSTNAME)) - override val port = MutableStateFlow(TEST_PORT) + override val httpPort = MutableStateFlow(TEST_PORT) + override val socksPort = MutableStateFlow(TEST_PORT + 1) override val broadcastType = MutableStateFlow(broadcastType) @@ -102,7 +116,8 @@ fun makeTestServerState( MutableStateFlow( BroadcastNetworkStatus.ConnectionInfo.Error( error = RuntimeException("Test Connection Error"))) - override val port = MutableStateFlow(TEST_PORT) + override val httpPort = MutableStateFlow(TEST_PORT) + override val socksPort = MutableStateFlow(TEST_PORT + 1) override val broadcastType = MutableStateFlow(broadcastType)