diff --git a/.gitignore b/.gitignore index aa724b7..aedf400 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .gradle /local.properties /.idea/caches +/.idea/deploymentTargetDropDown.xml /.idea/libraries /.idea/modules.xml /.idea/workspace.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0c338..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index b56bd63..441b1e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ByeDPI for Android -Application for Android that start a local VPN service to bypass DPI (Deep Packet Inspection) and unblock the internet +Application for Android that start a local VPN service to bypass DPI (Deep Packet Inspection) and censorship. ## Features diff --git a/app/.gitignore b/app/.gitignore index 4c49c4e..b7e35bd 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,5 @@ /build +/debug +/release *.aar *.jar \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f429cf4..db05b74 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "io.github.dovecoteescapee.byedpi" minSdk = 24 targetSdk = 34 - versionCode = 1 - versionName = "0.1.0-alpha" + versionCode = 2 + versionName = "0.1.1-alpha" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/libs/tun2socks b/app/libs/tun2socks index 792ee44..da7fa48 160000 --- a/app/libs/tun2socks +++ b/app/libs/tun2socks @@ -1 +1 @@ -Subproject commit 792ee44a8efa7ec4daadd2cbfda762329d1cbff2 +Subproject commit da7fa48a3784a5dca6714d4734a3eb3b181a96aa diff --git a/app/release/byedpi.apk b/app/release/byedpi.apk deleted file mode 100644 index 0b8e873..0000000 Binary files a/app/release/byedpi.apk and /dev/null differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json deleted file mode 100644 index c402b78..0000000 --- a/app/release/output-metadata.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "io.github.dovecoteescapee.byedpi", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 1, - "versionName": "0.1.0-alpha", - "outputFile": "app-release.apk" - } - ], - "elementType": "File" -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1719579..fc8effe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,7 +29,8 @@ + android:label="@string/title_settings" + android:exported="false"/> #include #include +#include #include +#include +#include -extern int big_loop(int fd); +extern int NOT_EXIT; struct packet fake_tls = { sizeof(tls_data), tls_data @@ -33,7 +36,7 @@ struct params params = { .max_open = 512, .bfsize = 16384, .baddr = { - .sin6_family = AF_INET6 + .sin6_family = AF_INET }, .debug = 2 }; @@ -55,8 +58,16 @@ int get_default_ttl() return orig_ttl; } -JNIEXPORT jint JNICALL -Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZIZ( +void *run(void *srv) { + LOG(LOG_S, "Start proxy thread"); + listener(*((struct sockaddr_ina *) srv)); + free(srv); + LOG(LOG_S, "Stop proxy thread"); + return NULL; +} + +JNIEXPORT jlong JNICALL +Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy( JNIEnv *env, jobject thiz, jint port, @@ -72,7 +83,8 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ jboolean host_mixed_case, jboolean domain_mixed_case, jboolean host_remove_space, - jint tls_record_split, + jboolean tls_record_split, + jint tls_record_split_position, jboolean tls_record_split_at_sni) { enum demode desync_methods[] = {DESYNC_NONE, DESYNC_SPLIT, DESYNC_DISORDER, DESYNC_FAKE}; @@ -89,6 +101,7 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ params.mod_http |= domain_mixed_case ? MH_DMIX : 0; params.mod_http |= host_remove_space ? MH_SPACE : 0; params.tlsrec = tls_record_split; + params.tlsrec_pos = tls_record_split_position; params.tlsrec_sni = tls_record_split_at_sni; if (!params.def_ttl && params.attack != DESYNC_NONE) { @@ -97,12 +110,23 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ } } - struct sockaddr_ina srv = { - .in = { - .sin_family = AF_INET, - .sin_port = htons(port), - } - }; + struct sockaddr_ina *srv = malloc(sizeof(struct sockaddr_ina)); + srv->in.sin_family = AF_INET; + srv->in.sin_addr.s_addr = inet_addr("0.0.0.0"); + srv->in.sin_port = htons(port); - return listener(srv); -} \ No newline at end of file + NOT_EXIT = 1; + + pthread_t proxy_thread; + if (pthread_create(&proxy_thread, NULL, run, srv) != 0) { + LOG(LOG_S, "Failed to start proxy thread"); + return -1; + } + + return proxy_thread; +} + +JNIEXPORT void JNICALL +Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStopProxy(JNIEnv *env, jobject thiz, jlong proxy_thread) { + NOT_EXIT = 0; +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt index 177d39e..f3583f3 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt @@ -14,7 +14,8 @@ class ByeDpiProxyPreferences( hostMixedCase: Boolean? = null, domainMixedCase: Boolean? = null, hostRemoveSpaces: Boolean? = null, - tlsRecordSplit: Int? = null, + tlsRecordSplit: Boolean? = null, + tlsRecordSplitPosition: Int? = null, tlsRecordSplitAtSni: Boolean? = null, ) { val port: Int = port ?: 1080 @@ -23,14 +24,15 @@ class ByeDpiProxyPreferences( val defaultTtl: Int = defaultTtl ?: 0 val noDomain: Boolean = noDomain ?: false val desyncKnown: Boolean = desyncKnown ?: false - val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.None + val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder val splitPosition: Int = splitPosition ?: 3 val splitAtHost: Boolean = splitAtHost ?: false val fakeTtl: Int = fakeTtl ?: 8 val hostMixedCase: Boolean = hostMixedCase ?: false val domainMixedCase: Boolean = domainMixedCase ?: false val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false - val tlsRecordSplit: Int = tlsRecordSplit ?: 0 + val tlsRecordSplit: Boolean = tlsRecordSplit ?: false + val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0 val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false enum class DesyncMethod { diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt index f3f954b..56b56b9 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt @@ -5,28 +5,26 @@ import android.content.Intent import android.net.VpnService import android.os.Build import android.os.IBinder +import android.os.ParcelFileDescriptor import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ServiceLifecycleDispatcher -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import engine.Engine import engine.Key -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - class ByeDpiVpnService : VpnService(), LifecycleOwner { - private val TAG: String = this::class.java.simpleName - private var proxyJob: Job? = null + private var proxyThread: Long = -1 + private var vpn: ParcelFileDescriptor? = null private val dispatcher = ServiceLifecycleDispatcher(this) override val lifecycle: Lifecycle get() = dispatcher.lifecycle companion object { + private val TAG: String = ByeDpiVpnService::class.java.simpleName + var status: Status = Status.STOPPED private set } @@ -48,6 +46,7 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { run() START_STICKY } + "stop" -> { stop() START_NOT_STICKY @@ -75,26 +74,30 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { } private fun run() { - val preferences = getPreferences(); - + val preferences = getPreferences() status = Status.RUNNING - if (proxyJob != null) { + if (proxyThread >= 0) { Log.w(TAG, "Proxy already running") return } - proxyJob = lifecycleScope.launch(Dispatchers.IO) { - runProxy(preferences) + proxyThread = startProxy(preferences) + if (proxyThread < 0) { + status = Status.STOPPED + Log.e(TAG, "Proxy failed to start") + return } val vpn = getBuilder().establish() + this.vpn = vpn if (vpn == null) { Log.e(TAG, "VPN connection failed") return } - startTun2Socks(vpn.detachFd(), preferences.port) + Log.d(TAG, "fd: ${vpn.fd}") + startTun2Socks(vpn.fd, preferences.port) } private fun getPreferences(): ByeDpiProxyPreferences { @@ -102,36 +105,38 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { return ByeDpiProxyPreferences( port = - sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(), + sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(), maxConnections = - sharedPreferences.getString("byedpi_max_connections", null)?.toInt(), + sharedPreferences.getString("byedpi_max_connections", null)?.toInt(), bufferSize = - sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(), + sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(), defaultTtl = - sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(), + sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(), noDomain = - sharedPreferences.getBoolean("byedpi_no_domain", false), + sharedPreferences.getBoolean("byedpi_no_domain", false), desyncKnown = - sharedPreferences.getBoolean("byedpi_desync_known", false), + sharedPreferences.getBoolean("byedpi_desync_known", false), desyncMethod = - sharedPreferences.getString("byedpi_desync_method", null) - ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) }, + sharedPreferences.getString("byedpi_desync_method", null) + ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) }, splitPosition = - sharedPreferences.getString("byedpi_split_position", null)?.toInt(), + sharedPreferences.getString("byedpi_split_position", null)?.toInt(), splitAtHost = - sharedPreferences.getBoolean("byedpi_split_at_host", false), + sharedPreferences.getBoolean("byedpi_split_at_host", false), fakeTtl = - sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(), + sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(), hostMixedCase = - sharedPreferences.getBoolean("byedpi_host_mixed_case", false), + sharedPreferences.getBoolean("byedpi_host_mixed_case", false), domainMixedCase = - sharedPreferences.getBoolean("byedpi_domain_mixed_case", false), + sharedPreferences.getBoolean("byedpi_domain_mixed_case", false), hostRemoveSpaces = - sharedPreferences.getBoolean("byedpi_host_remove_spaces", false), + sharedPreferences.getBoolean("byedpi_host_remove_spaces", false), tlsRecordSplit = - sharedPreferences.getString("byedpi_tlsrec", null)?.toInt(), + sharedPreferences.getBoolean("byedpi_tlsrec", false), + tlsRecordSplitPosition = + sharedPreferences.getString("byedpi_tlsrec_position", null)?.toInt(), tlsRecordSplitAtSni = - sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false), + sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false), ) } @@ -141,9 +146,9 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { stopProxy() } - private fun runProxy(preferences: ByeDpiProxyPreferences) : Int { + private fun startProxy(preferences: ByeDpiProxyPreferences): Long { Log.i(TAG, "Proxy started") - val res = startProxy( + return jniStartProxy( port = preferences.port, maxConnections = preferences.maxConnections, bufferSize = preferences.bufferSize, @@ -158,21 +163,20 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { domainMixedCase = preferences.domainMixedCase, hostRemoveSpace = preferences.hostRemoveSpaces, tlsRecordSplit = preferences.tlsRecordSplit, + tlsRecordSplitPosition = preferences.tlsRecordSplitPosition, tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni, ) - Log.i(TAG, "Proxy stopped") - return res } private fun stopProxy() { - proxyJob?.let { - Log.i(TAG, "Proxy stopped") - it.cancel() - proxyJob = null - } ?: Log.w(TAG, "Proxy not running") + if (proxyThread < 0) { + Log.w(TAG, "Proxy not running") + } + jniStopProxy(proxyThread) + proxyThread = -1 } - private fun startTun2Socks(fd: Int, port:Int) { + private fun startTun2Socks(fd: Int, port: Int) { val key = Key().apply { mark = 0 mtu = 0 @@ -197,10 +201,10 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { private fun stopTun2Socks() { Log.i(TAG, "Tun2socks stopped") - Engine.stop() + vpn?.close() ?: Log.w(TAG, "VPN not running") } - private external fun startProxy( + private external fun jniStartProxy( port: Int, maxConnections: Int, bufferSize: Int, @@ -214,9 +218,12 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { hostMixedCase: Boolean, domainMixedCase: Boolean, hostRemoveSpace: Boolean, - tlsRecordSplit: Int, + tlsRecordSplit: Boolean, + tlsRecordSplitPosition: Int, tlsRecordSplitAtSni: Boolean, - ): Int + ): Long + + private external fun jniStopProxy(proxyThread: Long) private fun getBuilder(): Builder { val builder = Builder() @@ -230,11 +237,13 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner { ) ) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val dns = sharedPreferences.getString("dns_ip", "9.9.9.9")!! + builder.addAddress("10.10.10.10", 32) builder.addRoute("0.0.0.0", 0) builder.addRoute("0:0:0:0:0:0:0:0", 0) - builder.addDnsServer("1.1.1.1") - builder.addDnsServer("1.0.0.1") + builder.addDnsServer(dns) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { builder.setMetered(false) } diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt index 184f67b..29e96cf 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt @@ -4,21 +4,23 @@ import android.content.Intent import android.net.VpnService import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.preference.PreferenceManager import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding - private val register = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - startVpnService() - } else { - Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show() + private val register = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + startVpnService() + } else { + Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show() + } } - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,19 +46,31 @@ class MainActivity : AppCompatActivity() { } } - binding.settingsButton.setOnClickListener { - val intent = Intent(this, SettingsActivity::class.java) - if (ByeDpiVpnService.status == Status.RUNNING) { - Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT) - .show() - } else { - startActivity(intent) - } - } - updateStatus(ByeDpiVpnService.status) } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + R.id.action_settings -> { + val intent = Intent(this, SettingsActivity::class.java) + if (ByeDpiVpnService.status == Status.RUNNING) { + Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT) + .show() + } else { + startActivity(intent) + } + + true + } + + else -> super.onOptionsItemSelected(item) + } + private fun startVpnService() { val intent = Intent(this, ByeDpiVpnService::class.java) intent.action = "start" @@ -67,6 +81,7 @@ class MainActivity : AppCompatActivity() { val intent = Intent(this, ByeDpiVpnService::class.java) intent.action = "stop" startService(intent) + stopService(intent) } private fun updateStatus(status : Status) { diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt index df414ae..9bdf42e 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt @@ -1,9 +1,65 @@ package io.github.dovecoteescapee.byedpi +import android.content.Intent import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.IOException class SettingsActivity : AppCompatActivity() { + private val register = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + lifecycleScope.launch(Dispatchers.IO) { + val logs = collectLogs() + + if (logs == null) { + Toast.makeText( + this@SettingsActivity, + R.string.logs_failed, + Toast.LENGTH_SHORT + ).show() + } else { + val uri = it.data?.data ?: run { + Log.e(TAG, "No data in result") + return@launch + } + contentResolver.openOutputStream(uri)?.use { + try { + it.write(logs.toByteArray()) + } catch (e: IOException) { + Log.e(TAG, "Failed to save logs", e) + } + } ?: run { + Log.e(TAG, "Failed to open output stream") + } + } + } + } + + + companion object { + private val TAG: String = SettingsActivity::class.java.simpleName + + private fun collectLogs(): String? = + try { + Runtime.getRuntime() + .exec("logcat *:I -d") + .inputStream.bufferedReader() + .use { it.readText() } + } catch (e: Exception) { + Log.e(TAG, "Failed to collect logs", e) + null + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) @@ -12,5 +68,48 @@ class SettingsActivity : AppCompatActivity() { .beginTransaction() .replace(R.id.settings, SettingsFragment()) .commit() + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_settings, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + + R.id.action_save_logs -> { + collectLogs() + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + setType("text/plain") + putExtra(Intent.EXTRA_TITLE, "byedpi.log") + } + + register.launch(intent) + true + } + + R.id.action_reset_settings -> { + PreferenceManager + .getDefaultSharedPreferences(this) + .edit() + .clear() + .apply() + + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, SettingsFragment()) + .commit() + true + } + + else -> super.onOptionsItemSelected(item) } } \ No newline at end of file diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt index 2fcc5d8..02f9d72 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt @@ -1,10 +1,61 @@ package io.github.dovecoteescapee.byedpi +import android.net.InetAddresses +import android.os.Build import android.os.Bundle +import android.util.Log +import android.util.Patterns +import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat class SettingsFragment : PreferenceFragmentCompat() { + companion object { + private val TAG: String = SettingsFragment::class.java.simpleName + + private fun checkIp(ip: String): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + InetAddresses.isNumericAddress(ip) + } else { + Patterns.IP_ADDRESS.matcher(ip).matches() + } + + private fun checkPort(port: String): Boolean = + port.toIntOrNull()?.let { it in 1..65535 } ?: false + } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings, rootKey) + + setEditTestPreferenceListener("dns_ip") { checkIp(it) } + setEditTestPreferenceListener("byedpi_port") { checkPort(it) } + setEditTestPreferenceListener("byedpi_max_connections") { value -> + value.toIntOrNull()?.let { it > 0 } ?: false + } + setEditTestPreferenceListener("byedpi_buffer_size") { value -> + value.toIntOrNull()?.let { it > 0 } ?: false + } + setEditTestPreferenceListener("byedpi_default_ttl") { value -> + value.toIntOrNull()?.let { it >= 0 } ?: false + } + setEditTestPreferenceListener("byedpi_split_position") { value -> + value.toIntOrNull() != null + } + setEditTestPreferenceListener("byedpi_fake_ttl") { value -> + value.toIntOrNull()?.let { it >= 0 } ?: false + } + setEditTestPreferenceListener("byedpi_tlsrec_position") { + it.toIntOrNull()?.let { it >= 0 } ?: false + } + } + + private fun setEditTestPreferenceListener(key: String, check: (String) -> Boolean) { + findPreference(key) + ?.setOnPreferenceChangeListener { preference, newValue -> + newValue as String + val valid = check(newValue) + if (!valid) { + Log.e(TAG, "Invalid ${preference.title}: $newValue") + } + valid + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..b240b83 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 45544b4..5d31311 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -23,16 +23,4 @@ app:layout_constraintTop_toTopOf="parent" style="@style/Widget.MaterialComponents.Button.OutlinedButton" /> - - diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..cf98ff6 --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_settings.xml b/app/src/main/res/menu/menu_settings.xml new file mode 100644 index 0000000..7b83655 --- /dev/null +++ b/app/src/main/res/menu/menu_settings.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..0e99879 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 397e633..a4a522a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,8 @@ Settings VPN permission denied Please stop the VPN service before changing settings + Settings + Save logs + Reset + Failed to collect logs \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e3796a2..bf9d93b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ - + - - \ No newline at end of file + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 3a48600..c318e6b 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -1,6 +1,13 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:tag="settings_screen"> + + @@ -82,10 +89,15 @@ android:title="Host remove spaces" android:defaultValue="false"/> + +