Release 0.1.1-alpha:

- Fix bug with UDP
- Fix some crashes
- Add DNS setting
- Add button reset settings
- Add log saving
- Update UI
This commit is contained in:
dovecoteescapee 2024-02-23 18:49:36 +03:00
parent cf8031115e
commit 54cc788ccd
23 changed files with 365 additions and 133 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
.gradle
/local.properties
/.idea/caches
/.idea/deploymentTargetDropDown.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

View File

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

2
app/.gitignore vendored
View File

@ -1,3 +1,5 @@
/build
/debug
/release
*.aar
*.jar

View File

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

@ -1 +1 @@
Subproject commit 792ee44a8efa7ec4daadd2cbfda762329d1cbff2
Subproject commit da7fa48a3784a5dca6714d4734a3eb3b181a96aa

Binary file not shown.

View File

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

View File

@ -29,7 +29,8 @@
<activity
android:name=".SettingsActivity"
android:exported="true" />
android:label="@string/title_settings"
android:exported="false"/>
<service android:name=".ByeDpiVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"

View File

@ -3,9 +3,12 @@
#include <params.h>
#include <packets.h>
#include <jni.h>
#include <android/log.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
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);
}
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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<EditTextPreference>(key)
?.setOnPreferenceChangeListener { preference, newValue ->
newValue as String
val valid = check(newValue)
if (!valid) {
Log.e(TAG, "Invalid ${preference.title}: $newValue")
}
valid
}
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@ -23,16 +23,4 @@
app:layout_constraintTop_toTopOf="parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<ImageButton
android:id="@+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/outline_settings_24"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="@android:color/transparent"
android:contentDescription="@string/settings"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="1"
android:title="@string/title_settings"
android:icon="@drawable/baseline_settings_24"
app:showAsAction="ifRoom" />
</menu>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_reset_settings"
android:orderInCategory="1"
android:title="@string/reset_settings"
app:showAsAction="never" />
<item
android:id="@+id/action_save_logs"
android:orderInCategory="2"
android:title="@string/save_logs"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,16 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/white</item>
<item name="colorPrimaryVariant">#E5E5E5</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#38AF42</item>
<item name="colorSecondaryVariant">#1F8C28</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -5,4 +5,8 @@
<string name="settings">Settings</string>
<string name="vpn_permission_denied">VPN permission denied</string>
<string name="settings_unavailable">Please stop the VPN service before changing settings</string>
<string name="title_settings">Settings</string>
<string name="save_logs">Save logs</string>
<string name="reset_settings">Reset</string>
<string name="logs_failed">Failed to collect logs</string>
</resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">#5976DF</item>
<item name="colorPrimaryVariant">#3C52A3</item>
@ -13,4 +13,4 @@
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
</resources>

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:tag="settings_screen">
<com.takisoft.preferencex.EditTextPreference
android:key="dns_ip"
android:title="DNS"
android:defaultValue="9.9.9.9"
app:useSimpleSummaryProvider="true"/>
<com.takisoft.preferencex.EditTextPreference
android:key="byedpi_port"
@ -45,13 +52,13 @@
android:title="Desync method"
android:entries="@array/byedpi_desync_methods"
android:entryValues="@array/byedpi_desync_methods_entries"
android:defaultValue="none"
android:defaultValue="disorder"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference
android:key="byedpi_split_position"
android:title="Split position"
android:inputType="number"
android:inputType="numberSigned"
android:defaultValue="3"
app:useSimpleSummaryProvider="true" />
@ -82,10 +89,15 @@
android:title="Host remove spaces"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="byedpi_tlsrec_enabled"
android:title="Split TLS record"
android:defaultValue="false"/>
<com.takisoft.preferencex.EditTextPreference
android:key="byedpi_tlsrec"
android:key="byedpi_tlsrec_position"
android:title="TLS record split position"
android:inputType="number"
android:inputType="numberSigned"
android:defaultValue="0"
app:useSimpleSummaryProvider="true" />