From c0af1a3eda473e905ad7ce9bc9efdff4f91d79a3 Mon Sep 17 00:00:00 2001 From: brightsunshine54 Date: Fri, 1 May 2026 17:09:07 +0700 Subject: [PATCH] Fix UI --- .../vpn/enums/BypassCensorshipMethod.java | 15 +- .../org/fptn/vpn/enums/SniSpoofingMode.java | 16 + .../speedtest/NativeSpeedTestTask.java | 8 +- .../services/speedtest/SpeedTestUtils.java | 5 +- .../fptn/vpn/services/vpn/FptnConnection.java | 7 +- .../fptn/vpn/services/vpn/FptnService.java | 14 +- .../websocket/NativeHttpsClientImpl.java | 33 +- .../websocket/NativeWebSocketClientImpl.java | 32 +- .../websocket/WebSocketClientWrapper.java | 12 +- .../org/fptn/vpn/utils/SharedPrefUtils.java | 20 +- .../bypassmethod/BypassMethodsActivity.java | 152 ++++------ .../bypassmethod/BypassMethodsViewModel.java | 12 + .../layout/settings_bypass_methods_layout.xml | 282 +++++++++--------- ...ner_item.xml => sni_mode_spinner_item.xml} | 12 +- app/src/main/res/values-fa-rIR/strings.xml | 1 - app/src/main/res/values-ru-rRU/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- .../org/fptn/vpn/core/common/Constants.kt | 1 + 18 files changed, 309 insertions(+), 319 deletions(-) create mode 100644 app/src/main/java/org/fptn/vpn/enums/SniSpoofingMode.java rename app/src/main/res/layout/{obfuscation_method_spinner_item.xml => sni_mode_spinner_item.xml} (57%) diff --git a/app/src/main/java/org/fptn/vpn/enums/BypassCensorshipMethod.java b/app/src/main/java/org/fptn/vpn/enums/BypassCensorshipMethod.java index d230c409..7b9418de 100644 --- a/app/src/main/java/org/fptn/vpn/enums/BypassCensorshipMethod.java +++ b/app/src/main/java/org/fptn/vpn/enums/BypassCensorshipMethod.java @@ -1,19 +1,6 @@ package org.fptn.vpn.enums; public enum BypassCensorshipMethod { - SNI_SPOOFING, // deprecated TLS_OBFUSCATION, - SNI_REALITY, // deprecated - /* Chrome */ - SNI_REALITY_CHROME_147, - SNI_REALITY_CHROME_146, - SNI_REALITY_CHROME_145, - /* Firefox */ - SNI_REALITY_FIREFOX_149, - /* Yandex Browser */ - SNI_REALITY_YANDEX_26, - SNI_REALITY_YANDEX_25, - SNI_REALITY_YANDEX_24, - /* Safari */ - SNI_REALITY_SAFARI_26 + SNI_REALITY } diff --git a/app/src/main/java/org/fptn/vpn/enums/SniSpoofingMode.java b/app/src/main/java/org/fptn/vpn/enums/SniSpoofingMode.java new file mode 100644 index 00000000..d8c412cd --- /dev/null +++ b/app/src/main/java/org/fptn/vpn/enums/SniSpoofingMode.java @@ -0,0 +1,16 @@ +package org.fptn.vpn.enums; + +public enum SniSpoofingMode { + /* Chrome */ + SNI_REALITY_CHROME_147, + SNI_REALITY_CHROME_146, + SNI_REALITY_CHROME_145, + /* Firefox */ + SNI_REALITY_FIREFOX_149, + /* Yandex Browser */ + SNI_REALITY_YANDEX_26, + SNI_REALITY_YANDEX_25, + SNI_REALITY_YANDEX_24, + /* Safari */ + SNI_REALITY_SAFARI_26 +} diff --git a/app/src/main/java/org/fptn/vpn/services/speedtest/NativeSpeedTestTask.java b/app/src/main/java/org/fptn/vpn/services/speedtest/NativeSpeedTestTask.java index a3e08b9f..8d1814f5 100644 --- a/app/src/main/java/org/fptn/vpn/services/speedtest/NativeSpeedTestTask.java +++ b/app/src/main/java/org/fptn/vpn/services/speedtest/NativeSpeedTestTask.java @@ -1,9 +1,8 @@ package org.fptn.vpn.services.speedtest; -import android.util.Log; - import org.fptn.vpn.database.entity.ServerEntity; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.websocket.NativeHttpsClientImpl; import org.fptn.vpn.services.websocket.NativeResponse; import org.fptn.vpn.vpnclient.exception.PVNClientException; @@ -22,14 +21,15 @@ public class NativeSpeedTestTask implements Callable { private final ServerEntity serverEntity; private final NativeHttpsClientImpl nativeHttpsClient; - public NativeSpeedTestTask(ServerEntity serverEntity, String sniHost, BypassCensorshipMethod censorshipStrategy) { + public NativeSpeedTestTask(ServerEntity serverEntity, String sniHost, BypassCensorshipMethod censorshipStrategy, SniSpoofingMode sniSpoofingMode) { this.serverEntity = serverEntity; this.nativeHttpsClient = new NativeHttpsClientImpl( serverEntity.getHost(), serverEntity.getPort(), serverEntity.getMd5ServerFingerprint(), sniHost, - censorshipStrategy + censorshipStrategy, + sniSpoofingMode ); } diff --git a/app/src/main/java/org/fptn/vpn/services/speedtest/SpeedTestUtils.java b/app/src/main/java/org/fptn/vpn/services/speedtest/SpeedTestUtils.java index 78a88af8..259b8964 100644 --- a/app/src/main/java/org/fptn/vpn/services/speedtest/SpeedTestUtils.java +++ b/app/src/main/java/org/fptn/vpn/services/speedtest/SpeedTestUtils.java @@ -4,6 +4,7 @@ import org.fptn.vpn.database.entity.ServerEntity; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.vpnclient.exception.ErrorCode; import org.fptn.vpn.vpnclient.exception.PVNClientException; @@ -23,7 +24,7 @@ public class SpeedTestUtils { private static final long SEARCH_BEST_SERVER_MAX_TIMEOUT = 30L; private static final String PREMIUM_KEYWORD = "premium"; - public static ServerEntity findFastestServer(List serverEntityList, String sniHostName, BypassCensorshipMethod censorshipStrategy) throws PVNClientException { + public static ServerEntity findFastestServer(List serverEntityList, String sniHostName, BypassCensorshipMethod censorshipStrategy, SniSpoofingMode sniSpoofingMode) throws PVNClientException { Log.d(TAG, "SpeedTestUtils.findFastestServer() start: " + Instant.now() + ", Thread.Id: " + Thread.currentThread().getId()); if (serverEntityList != null && !serverEntityList.isEmpty()) { @@ -33,7 +34,7 @@ public static ServerEntity findFastestServer(List serverEntityList ExecutorService executor = Executors.newFixedThreadPool(selectedServers.size()); List nativeSpeedTestTaskList = selectedServers.stream() - .map(fptnServerDto -> new NativeSpeedTestTask(fptnServerDto, sniHostName, censorshipStrategy)) + .map(fptnServerDto -> new NativeSpeedTestTask(fptnServerDto, sniHostName, censorshipStrategy, sniSpoofingMode)) .collect(Collectors.toList()); try { NativeSpeedTestResult bestResult = executor.invokeAny(nativeSpeedTestTaskList, SEARCH_BEST_SERVER_MAX_TIMEOUT, TimeUnit.SECONDS); diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java index c5e92a7c..4ad5adb2 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java @@ -21,6 +21,7 @@ import org.fptn.vpn.enums.ConnectionState; import org.fptn.vpn.enums.NetworkType; import org.fptn.vpn.enums.PerAppVpnMode; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.websocket.DnsServers; import org.fptn.vpn.services.websocket.WebSocketAlreadyShutdownException; import org.fptn.vpn.services.websocket.WebSocketClientWrapper; @@ -80,6 +81,7 @@ public class FptnConnection extends Thread { @Getter private final AtomicInteger reconnectCount = new AtomicInteger(0); + private final SniSpoofingMode sniSpoofingMode; @Setter private PendingIntent configureVpnIntent; @@ -114,6 +116,7 @@ public FptnConnection(final FptnService service, final int delayBetweenAttempts, final String sniHostName, final BypassCensorshipMethod censorshipStrategy, + final SniSpoofingMode sniSpoofingMode, final PerAppVpnMode perAppVpnMode, final List appInfos) throws UnknownHostException { this.service = service; @@ -123,6 +126,7 @@ public FptnConnection(final FptnService service, this.currentNetworkType = currentNetworkType; this.sniHostName = sniHostName; this.censorshipStrategy = censorshipStrategy; + this.sniSpoofingMode = sniSpoofingMode; this.perAppVpnMode = perAppVpnMode; this.appInfos = appInfos; @@ -135,7 +139,8 @@ public FptnConnection(final FptnService service, this::onMessageReceived, this::onConnectionFailure, this.sniHostName, - this.censorshipStrategy + this.censorshipStrategy, + this.sniSpoofingMode ); this.maxReconnectCount = maxReconnectCount; diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java index 99bdf654..fa00905d 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java @@ -37,6 +37,7 @@ import org.fptn.vpn.enums.ConnectionState; import org.fptn.vpn.enums.NetworkType; import org.fptn.vpn.enums.PerAppVpnMode; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.tile.FptnTileService; import org.fptn.vpn.utils.NetworkUtils; import org.fptn.vpn.utils.NotificationUtils; @@ -243,6 +244,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { String sniHostname = SharedPrefUtils.getSniHostname(getApplicationContext()); BypassCensorshipMethod bypassCensorshipMethod = SharedPrefUtils.getBypassCensorshipMethod(this); + SniSpoofingMode sniSpoofingMode = null; + if (bypassCensorshipMethod == BypassCensorshipMethod.SNI_REALITY){ + sniSpoofingMode = SharedPrefUtils.getSniSpoofingMode(this); + } + int serverId = intent.getIntExtra(SELECTED_SERVER, SELECTED_SERVER_ID_AUTO); // Process startService from TileService @@ -272,7 +278,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { updateNotificationWithMessage(getString(R.string.connecting_auto), ""); List serverEntities = appDatabase.serverDAO().getServerList(false); - ServerEntity server = SpeedTestUtils.findFastestServer(serverEntities, sniHostname, bypassCensorshipMethod); + ServerEntity server = SpeedTestUtils.findFastestServer(serverEntities, sniHostname, bypassCensorshipMethod, sniSpoofingMode); setSelectedServer(server.getId()); connect(server, sniHostname); @@ -451,6 +457,11 @@ private void connect(ServerEntity serverEntity, String sniHostname) throws Unkno BypassCensorshipMethod bypassCensorshipMethod = SharedPrefUtils.getBypassCensorshipMethod(this); + SniSpoofingMode sniSpoofingMode = null; + if (bypassCensorshipMethod == BypassCensorshipMethod.SNI_REALITY){ + sniSpoofingMode = SharedPrefUtils.getSniSpoofingMode(this); + } + PerAppVpnMode perAppVpnMode = SharedPrefUtils.getPerAppVPNMode(this); List appInfos = new ArrayList<>(); if (perAppVpnMode == PerAppVpnMode.ONLY_ALLOWED || perAppVpnMode == PerAppVpnMode.EXCEPT_DISALLOWED) { @@ -478,6 +489,7 @@ private void connect(ServerEntity serverEntity, String sniHostname) throws Unkno delayBetweenAttempts, sniHostname, bypassCensorshipMethod, + sniSpoofingMode, perAppVpnMode, appInfos ); diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/NativeHttpsClientImpl.java b/app/src/main/java/org/fptn/vpn/services/websocket/NativeHttpsClientImpl.java index 2e4824d4..8b9c460f 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/NativeHttpsClientImpl.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/NativeHttpsClientImpl.java @@ -3,6 +3,7 @@ import android.util.Log; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; public class NativeHttpsClientImpl { private static final String TAG = NativeHttpsClientImpl.class.getName(); @@ -17,37 +18,15 @@ public NativeHttpsClientImpl(String serverIP, int serverPort, String md5Fingerprint, String sni, - BypassCensorshipMethod censorshipStrategy) { + BypassCensorshipMethod censorshipStrategy, + SniSpoofingMode sniSpoofingMode) { String censorshipStrategyName = "SNI-REALITY-YANDEX-25"; if (censorshipStrategy == BypassCensorshipMethod.TLS_OBFUSCATION) { censorshipStrategyName = "OBFUSCATION"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY) { // deprecated - censorshipStrategyName = "SNI-REALITY"; - } - /* Chrome */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_147) { - censorshipStrategyName = "SNI-REALITY-CHROME-147"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_146) { - censorshipStrategyName = "SNI-REALITY-CHROME-146"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_145) { - censorshipStrategyName = "SNI-REALITY-CHROME-145"; - } - /* Firefox */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_FIREFOX_149) { - censorshipStrategyName = "SNI-REALITY-FIREFOX-149"; - } - /* Yandex */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_26) { - censorshipStrategyName = "SNI-REALITY-YANDEX-26"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_25) { - censorshipStrategyName = "SNI-REALITY-YANDEX-25"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_24) { - censorshipStrategyName = "SNI-REALITY-YANDEX-24"; - } - /* Safari */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_SAFARI_26) { - censorshipStrategyName = "SNI-REALITY-SAFARI-26"; + } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY) { + censorshipStrategyName = sniSpoofingMode.toString().replace('_', '-'); } + this.nativeHandle = nativeCreate( serverIP, serverPort, diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java b/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java index 6da0ea4f..fd647957 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java @@ -3,6 +3,7 @@ import android.util.Log; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.websocket.callback.OnFailureCallback; import org.fptn.vpn.services.websocket.callback.OnMessageReceivedCallback; import org.fptn.vpn.services.websocket.callback.OnOpenCallback; @@ -39,7 +40,8 @@ public NativeWebSocketClientImpl( OnMessageReceivedCallback onMessageReceivedCallback, OnFailureCallback onFailureCallback, String sniHostName, - BypassCensorshipMethod censorshipStrategy) throws PVNClientException { + BypassCensorshipMethod censorshipStrategy, + SniSpoofingMode sniSpoofingMode) throws PVNClientException { this.onOpenCallback = onOpenCallback; this.onMessageReceivedCallback = onMessageReceivedCallback; this.onFailureCallback = onFailureCallback; @@ -47,32 +49,8 @@ public NativeWebSocketClientImpl( String censorshipStrategyName = "SNI-REALITY-YANDEX-25"; if (censorshipStrategy == BypassCensorshipMethod.TLS_OBFUSCATION) { censorshipStrategyName = "OBFUSCATION"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY) { // deprecated - censorshipStrategyName = "SNI-REALITY"; - } - /* Chrome */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_147) { - censorshipStrategyName = "SNI-REALITY-CHROME-147"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_146) { - censorshipStrategyName = "SNI-REALITY-CHROME-146"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_CHROME_145) { - censorshipStrategyName = "SNI-REALITY-CHROME-145"; - } - /* Firefox */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_FIREFOX_149) { - censorshipStrategyName = "SNI-REALITY-FIREFOX-149"; - } - /* Yandex */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_26) { - censorshipStrategyName = "SNI-REALITY-YANDEX-26"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_25) { - censorshipStrategyName = "SNI-REALITY-YANDEX-25"; - } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_YANDEX_24) { - censorshipStrategyName = "SNI-REALITY-YANDEX-24"; - } - /* Safari */ - else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY_SAFARI_26) { - censorshipStrategyName = "SNI-REALITY-SAFARI-26"; + } else if (censorshipStrategy == BypassCensorshipMethod.SNI_REALITY) { + censorshipStrategyName = sniSpoofingMode.toString().replace('_', '-'); } this.nativeHandle = nativeCreate( diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java index 3bed5863..2eb44e2f 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java @@ -6,6 +6,7 @@ import org.fptn.vpn.database.entity.ServerEntity; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.websocket.callback.OnFailureCallback; import org.fptn.vpn.services.websocket.callback.OnMessageReceivedCallback; import org.fptn.vpn.services.websocket.callback.OnOpenCallback; @@ -29,6 +30,7 @@ public class WebSocketClientWrapper { private final NativeHttpsClientImpl nativeHttpsClient; private final String sniHostName; private final BypassCensorshipMethod censorshipStrategy; + private final SniSpoofingMode sniSpoofingMode; private NativeWebSocketClientImpl nativeWebSocketClient; @@ -42,7 +44,8 @@ public WebSocketClientWrapper(ServerEntity serverEntity, OnMessageReceivedCallback onMessageReceivedCallback, OnFailureCallback onFailureCallback, String sniHostName, - BypassCensorshipMethod censorshipStrategy) { + BypassCensorshipMethod censorshipStrategy, + SniSpoofingMode sniSpoofingMode) { this.serverEntity = serverEntity; this.tunAddressIPv4 = tunAddressIPv4; this.tunAddressIPv6 = tunAddressIPv6; @@ -53,13 +56,15 @@ public WebSocketClientWrapper(ServerEntity serverEntity, // this is SNI spoofing this.sniHostName = sniHostName; this.censorshipStrategy = censorshipStrategy; + this.sniSpoofingMode = sniSpoofingMode; this.nativeHttpsClient = new NativeHttpsClientImpl( serverEntity.getHost(), serverEntity.getPort(), serverEntity.getMd5ServerFingerprint(), sniHostName, - censorshipStrategy + censorshipStrategy, + sniSpoofingMode ); } @@ -82,7 +87,8 @@ public synchronized void startWebSocket() throws PVNClientException, WebSocketAl onMessageReceivedCallback, onFailureCallback, sniHostName, - censorshipStrategy + censorshipStrategy, + sniSpoofingMode ); Log.d(getTag(), "startWebSocket() nativeWebSocketClient.start() Thread.id: " + Thread.currentThread().getId()); diff --git a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java index e9d17be6..77eb3b8c 100644 --- a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java +++ b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java @@ -7,6 +7,7 @@ import org.fptn.vpn.core.common.Constants; import org.fptn.vpn.enums.BypassCensorshipMethod; import org.fptn.vpn.enums.PerAppVpnMode; +import org.fptn.vpn.enums.SniSpoofingMode; import java.util.Objects; @@ -127,7 +128,7 @@ public static BypassCensorshipMethod getBypassCensorshipMethod(Context context) return value; } } - return BypassCensorshipMethod.SNI_SPOOFING; + return BypassCensorshipMethod.SNI_REALITY; } public static void saveBypassCensorshipMethod(Context context, BypassCensorshipMethod method) { @@ -135,6 +136,23 @@ public static void saveBypassCensorshipMethod(Context context, BypassCensorshipM sharedPreferences.edit().putString(Constants.BYPASS_CENSORSHIP_METHOD_SHARED_PREF_KEY, method.toString()).apply(); } + + public static SniSpoofingMode getSniSpoofingMode(Context context) { + SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); + String modeName = sharedPreferences.getString(Constants.SNI_SPOOFING_MODE_SHARED_PREF_KEY, null); + for (SniSpoofingMode value : SniSpoofingMode.values()) { + if (Objects.equals(modeName, value.name())) { + return value; + } + } + return SniSpoofingMode.SNI_REALITY_YANDEX_25; + } + + public static void saveSniSpoofingMode(Context context, SniSpoofingMode mode) { + SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); + sharedPreferences.edit().putString(Constants.SNI_SPOOFING_MODE_SHARED_PREF_KEY, mode.toString()).apply(); + } + /* Per-app VPN settings */ public static PerAppVpnMode getPerAppVPNMode(Context context) { SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); diff --git a/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsActivity.java b/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsActivity.java index 770cc160..8db53c7a 100644 --- a/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsActivity.java +++ b/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsActivity.java @@ -13,6 +13,8 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.ProgressBar; @@ -37,6 +39,7 @@ import org.fptn.vpn.R; import org.fptn.vpn.database.entity.ServerEntity; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.snichecker.SniCheckerService; import org.fptn.vpn.services.snichecker.SniCheckerServiceState; import org.fptn.vpn.utils.ViewUtils; @@ -109,6 +112,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { private void initializeVariable() { viewModel = new ViewModelProvider(this).get(BypassMethodsViewModel.class); + sniLayout = findViewById(R.id.sni_layout); + BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavBar); bottomNavigationView.setSelectedItemId(R.id.menuSettings); bottomNavigationView.setOnItemSelectedListener(new CustomBottomNavigationListener(this, R.id.menuSettings)); @@ -116,113 +121,76 @@ private void initializeVariable() { // Setup RadioGroup listener RadioGroup protocolRadioGroup = findViewById(R.id.bypass_method_radio_button_group); protocolRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { - if (checkedId == R.id.obfuscation_radio_button) { + if (checkedId == R.id.sni_reality_radio_button) { + Log.d(TAG, "Selected SNI spoofing"); + viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY); + } else if (checkedId == R.id.obfuscation_radio_button) { Log.d(TAG, "Selected TLS obfuscation"); viewModel.setBypassMethod(BypassCensorshipMethod.TLS_OBFUSCATION); } - /* Chrome */ - else if (checkedId == R.id.sni_reality_radio_button_chrome_147) { - Log.d(TAG, "Selected SNI Reality Chrome 147"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_CHROME_147); - } else if (checkedId == R.id.sni_reality_radio_button_chrome_146) { - Log.d(TAG, "Selected SNI Reality Chrome 146"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_CHROME_146); - } else if (checkedId == R.id.sni_reality_radio_button_chrome_145) { - Log.d(TAG, "Selected SNI Reality Chrome 145"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_CHROME_145); - } - /* Firefox */ - else if (checkedId == R.id.sni_reality_radio_button_firefox_149) { - Log.d(TAG, "Selected SNI Reality Firefox 149"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_FIREFOX_149); - } - /* Yandex */ - else if (checkedId == R.id.sni_reality_radio_button_yandex_26) { - Log.d(TAG, "Selected SNI Reality Yandex 26"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_YANDEX_26); - } else if (checkedId == R.id.sni_reality_radio_button_yandex_25) { - Log.d(TAG, "Selected SNI Reality Yandex 25"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_YANDEX_25); - } else if (checkedId == R.id.sni_reality_radio_button_yandex_24) { - Log.d(TAG, "Selected SNI Reality Yandex 24"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_YANDEX_24); - } - /* Safari */ - else if (checkedId == R.id.sni_reality_radio_button_safari_26) { - Log.d(TAG, "Selected SNI Reality Safari 26"); - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_SAFARI_26); - } else { - // default - viewModel.setBypassMethod(BypassCensorshipMethod.SNI_REALITY_YANDEX_25); - } }); + RadioButton sniSpoofingRadioButton = findViewById(R.id.sni_reality_radio_button); RadioButton obfuscationRadioButton = findViewById(R.id.obfuscation_radio_button); - RadioButton sniRealityRadioButtonChrome147 = findViewById(R.id.sni_reality_radio_button_chrome_147); - RadioButton sniRealityRadioButtonChrome146 = findViewById(R.id.sni_reality_radio_button_chrome_146); - RadioButton sniRealityRadioButtonChrome145 = findViewById(R.id.sni_reality_radio_button_chrome_145); - - RadioButton sniRealityRadioButtonFirefox149 = findViewById(R.id.sni_reality_radio_button_firefox_149); - - RadioButton sniRealityRadioButtonYandex26 = findViewById(R.id.sni_reality_radio_button_yandex_26); - RadioButton sniRealityRadioButtonYandex25 = findViewById(R.id.sni_reality_radio_button_yandex_25); - RadioButton sniRealityRadioButtonYandex24 = findViewById(R.id.sni_reality_radio_button_yandex_24); - - RadioButton sniRealityRadioButtonSafari26 = findViewById(R.id.sni_reality_radio_button_safari_26); - viewModel.getBypassCensorshipMethodMutableLiveData().observe(this, bypassCensorshipMethod -> { switch (bypassCensorshipMethod) { case TLS_OBFUSCATION: obfuscationRadioButton.setChecked(true); ViewUtils.hideView(sniLayout); break; - case SNI_SPOOFING: // deprecated - case SNI_REALITY: // deprecated - /* Yandex */ - case SNI_REALITY_YANDEX_25: - sniRealityRadioButtonYandex25.setChecked(true); - ViewUtils.showView(sniLayout); - break; - case SNI_REALITY_YANDEX_26: - sniRealityRadioButtonYandex26.setChecked(true); - ViewUtils.showView(sniLayout); - break; - case SNI_REALITY_YANDEX_24: - sniRealityRadioButtonYandex24.setChecked(true); - ViewUtils.showView(sniLayout); - break; - /* Chrome */ - case SNI_REALITY_CHROME_147: - sniRealityRadioButtonChrome147.setChecked(true); - ViewUtils.showView(sniLayout); - break; - case SNI_REALITY_CHROME_146: - sniRealityRadioButtonChrome146.setChecked(true); - ViewUtils.showView(sniLayout); - break; - case SNI_REALITY_CHROME_145: - sniRealityRadioButtonChrome145.setChecked(true); - ViewUtils.showView(sniLayout); - break; - /* Firefox */ - case SNI_REALITY_FIREFOX_149: - sniRealityRadioButtonFirefox149.setChecked(true); - ViewUtils.showView(sniLayout); - break; - /* Safari */ - case SNI_REALITY_SAFARI_26: - sniRealityRadioButtonSafari26.setChecked(true); - ViewUtils.showView(sniLayout); - break; - default: - sniRealityRadioButtonYandex25.setChecked(true); + case SNI_REALITY: + sniSpoofingRadioButton.setChecked(true); ViewUtils.showView(sniLayout); break; } }); + // Inside initializeVariable() method + Spinner sniSpoofingModeSpinner = findViewById(R.id.sni_spoofing_mode_spinner); + ArrayAdapter adapter = new ArrayAdapter<>( + this, + R.layout.sni_mode_spinner_item, + R.id.sni_mode_label, + SniSpoofingMode.values() + ){ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView textView = (TextView) super.getView(position, convertView, parent); + textView.setText(getSniSpoofingModeFriendlyName(getItem(position))); + return textView; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + TextView textView = (TextView) super.getDropDownView(position, convertView, parent); + textView.setText(getSniSpoofingModeFriendlyName(getItem(position))); + return textView; + } + }; + sniSpoofingModeSpinner.setAdapter(adapter); + + viewModel.getSniSpoofingModeMutableLiveData().observe(this, mode -> { + int position = adapter.getPosition(mode); + if (position >= 0) { + sniSpoofingModeSpinner.setSelection(position); + } + }); + + sniSpoofingModeSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + SniSpoofingMode selectedMode = (SniSpoofingMode) parent.getItemAtPosition(position); + viewModel.setSniSpoofingMode(selectedMode); + } + + @Override + public void onNothingSelected(android.widget.AdapterView parent) { + } + }); + + // SNI field TextView sniTextField = findViewById(R.id.SNI_text_field); viewModel.getSniMutableLiveData().observe(this, sniTextField::setText); @@ -458,4 +426,12 @@ public void onEditSNIServer(View view) { }); alertDialogBuilder.show(); } + + private String getSniSpoofingModeFriendlyName(SniSpoofingMode mode) { + if (mode == null) return ""; + String resourceName = mode.name().toLowerCase() + .replace("sni_reality_", "sni_reality_radio_button_label_"); + int resId = getResources().getIdentifier(resourceName, "string", getPackageName()); + return resId != 0 ? getString(resId) : mode.toString(); + } } diff --git a/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsViewModel.java b/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsViewModel.java index 76ea513a..7bd97739 100644 --- a/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsViewModel.java +++ b/app/src/main/java/org/fptn/vpn/views/bypassmethod/BypassMethodsViewModel.java @@ -18,6 +18,7 @@ import org.fptn.vpn.database.entity.ServerEntity; import org.fptn.vpn.database.entity.SniEntity; import org.fptn.vpn.enums.BypassCensorshipMethod; +import org.fptn.vpn.enums.SniSpoofingMode; import org.fptn.vpn.services.snichecker.SniCheckerService; import org.fptn.vpn.services.snichecker.SniCheckerServiceState; import org.fptn.vpn.utils.SharedPrefUtils; @@ -55,6 +56,9 @@ public class BypassMethodsViewModel extends AndroidViewModel { @Getter private final MutableLiveData selectedServer = new MutableLiveData<>(ServerEntity.AUTO); + @Getter + private final MutableLiveData sniSpoofingModeMutableLiveData; + private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication()); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -63,6 +67,7 @@ public BypassMethodsViewModel(@NonNull Application application) { sniMutableLiveData = new MutableLiveData<>(SharedPrefUtils.getSniHostname(application)); bypassCensorshipMethodMutableLiveData = new MutableLiveData<>(SharedPrefUtils.getBypassCensorshipMethod(application)); + sniSpoofingModeMutableLiveData = new MutableLiveData<>(SharedPrefUtils.getSniSpoofingMode(application)); refreshSniCount(); } @@ -88,10 +93,17 @@ public void setBypassMethod(BypassCensorshipMethod bypassMethod) { bypassCensorshipMethodMutableLiveData.postValue(bypassMethod); } + public void setSniSpoofingMode(SniSpoofingMode sniSpoofingMode) { + sniSpoofingModeMutableLiveData.postValue(sniSpoofingMode); + } + public void saveBypassMethod() { BypassCensorshipMethod bypassCensorshipMethod = bypassCensorshipMethodMutableLiveData.getValue(); if (bypassCensorshipMethod != null) { SharedPrefUtils.saveBypassCensorshipMethod(getApplication(), bypassCensorshipMethod); + if (bypassCensorshipMethod == BypassCensorshipMethod.SNI_REALITY) { + SharedPrefUtils.saveSniSpoofingMode(getApplication(), sniSpoofingModeMutableLiveData.getValue()); + } } } diff --git a/app/src/main/res/layout/settings_bypass_methods_layout.xml b/app/src/main/res/layout/settings_bypass_methods_layout.xml index 98557766..380a030d 100644 --- a/app/src/main/res/layout/settings_bypass_methods_layout.xml +++ b/app/src/main/res/layout/settings_bypass_methods_layout.xml @@ -9,7 +9,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/obfuscation_method_spinner_item.xml b/app/src/main/res/layout/sni_mode_spinner_item.xml similarity index 57% rename from app/src/main/res/layout/obfuscation_method_spinner_item.xml rename to app/src/main/res/layout/sni_mode_spinner_item.xml index 9fc441b9..e36d9c9a 100644 --- a/app/src/main/res/layout/obfuscation_method_spinner_item.xml +++ b/app/src/main/res/layout/sni_mode_spinner_item.xml @@ -1,9 +1,11 @@ + \ No newline at end of file + android:singleLine="true" /> \ No newline at end of file diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index a141fbdd..37abeebc 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -159,7 +159,6 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn روش‌های دور زدن محدودیت‌ها پیکربندی روش‌های دور زدن فیلترینگ - جعل دامنه (SNI) مخفی‌سازی ترافیک (Obfuscation) جعل پیشرفتهٔ دامنه (SNI + REALITY) diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 416e84a9..35eb1e45 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -156,9 +156,8 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn Методы обхода блокировок Настройка методов обхода блокировок - Подмена домена (SNI) Маскировка трафика (обфускация) - Продвинутая подмена домена (SNI + REALITY) + Подмена домена (SNI + REALITY) Подмена домена (Chrome 147) Подмена домена (Chrome 146) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ccc3177b..4aaf25b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,9 +226,8 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn Bypass blocking methods Configure bypass blocking methods - Domain spoofing (SNI) Traffic masking (obfuscation) - Advanced domain spoofing (SNI + REALITY) + Domain spoofing (SNI + REALITY) Domain spoofing (Chrome 147) Domain spoofing (Chrome 146) diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt index 605d779c..6418c8c9 100644 --- a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt @@ -39,5 +39,6 @@ object Constants { const val RECONNECT_DELAY_BETWEEN_SHARED_PREF_KEY: String = "RECONNECT_DELAY_BETWEEN_SHARED_PREF_KEY" const val BYPASS_CENSORSHIP_METHOD_SHARED_PREF_KEY: String = "BYPASS_CENSORSHIP_METHOD_SHARED_PREF_KEY" + const val SNI_SPOOFING_MODE_SHARED_PREF_KEY: String = "SNI_SPOOFING_MODE_SHARED_PREF_KEY" const val PER_APP_VPN_MODE_SHARED_PREF_KEY: String = "PER_APP_VPN_MODE_SHARED_PREF_KEY" }