mirror of
https://github.com/dovecoteescapee/ByeDPIAndroid.git
synced 2024-12-21 22:06:22 +00:00
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:
parent
cf8031115e
commit
54cc788ccd
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
|
@ -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>
|
@ -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
2
app/.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/build
|
||||
/debug
|
||||
/release
|
||||
*.aar
|
||||
*.jar
|
@ -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.
@ -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"
|
||||
}
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/baseline_settings_24.xml
Normal file
5
app/src/main/res/drawable/baseline_settings_24.xml
Normal 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>
|
@ -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>
|
||||
|
14
app/src/main/res/menu/menu_main.xml
Normal file
14
app/src/main/res/menu/menu_main.xml
Normal 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>
|
19
app/src/main/res/menu/menu_settings.xml
Normal file
19
app/src/main/res/menu/menu_settings.xml
Normal 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>
|
16
app/src/main/res/values-night/themes.xml
Normal file
16
app/src/main/res/values-night/themes.xml
Normal 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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user