diff --git a/.exists b/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/.exists b/app/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..e1eb950
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,40 @@
+plugins {
+ alias(libs.plugins.android.application)
+}
+
+android {
+ namespace 'me.sergiotarxz.bedrockstation'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "me.sergiotarxz.bedrockstation"
+ minSdk 33
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+
+ implementation libs.appcompat
+ implementation libs.material
+ implementation libs.activity
+ implementation libs.constraintlayout
+ testImplementation libs.junit
+ androidTestImplementation libs.ext.junit
+ androidTestImplementation libs.espresso.core
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/.exists b/app/src/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/.exists b/app/src/androidTest/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/java/.exists b/app/src/androidTest/java/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/java/me/.exists b/app/src/androidTest/java/me/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/java/me/sergiotarxz/.exists b/app/src/androidTest/java/me/sergiotarxz/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/java/me/sergiotarxz/bedrockstation/.exists b/app/src/androidTest/java/me/sergiotarxz/bedrockstation/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/androidTest/java/me/sergiotarxz/bedrockstation/ExampleInstrumentedTest.java b/app/src/androidTest/java/me/sergiotarxz/bedrockstation/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..edeb118
--- /dev/null
+++ b/app/src/androidTest/java/me/sergiotarxz/bedrockstation/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package me.sergiotarxz.bedrockstation;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("me.sergiotarxz.bedrockstation", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/.exists b/app/src/main/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5e88685
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/.exists b/app/src/main/java/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/me/.exists b/app/src/main/java/me/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/me/sergiotarxz/.exists b/app/src/main/java/me/sergiotarxz/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/.exists b/app/src/main/java/me/sergiotarxz/bedrockstation/.exists
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/DB.java b/app/src/main/java/me/sergiotarxz/bedrockstation/DB.java
new file mode 100644
index 0000000..d214728
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/DB.java
@@ -0,0 +1,49 @@
+package me.sergiotarxz.bedrockstation;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.content.Context;
+
+public class DB extends SQLiteOpenHelper {
+ public static final String DATABASE_NAME = "bedrockstation.sqlite3";
+
+ public static final String[] MIGRATIONS = {
+ "CREATE TABLE options (\n"
+ + "id INTEGER PRIMARY KEY,\n"
+ + "key TEXT UNIQUE,\n"
+ + "value TEXT\n"
+ + ");",
+ "INSERT OR IGNORE INTO options(key, value) VALUES(\"host_cache\", \"192.168.2.1\");",
+ "INSERT OR IGNORE INTO options(key, value) VALUES(\"port_cache\", \"19132\");",
+ };
+
+ public DB(Context context) {
+ super(context, DATABASE_NAME, null, MIGRATIONS.length);
+ }
+
+ private SQLiteDatabase db = null;
+
+ public SQLiteDatabase getInstance() {
+ if (db == null) {
+ db = this.getWritableDatabase();
+ }
+ return db;
+ }
+
+ public void onCreate(SQLiteDatabase db) {
+ onUpgrade(db, 0, MIGRATIONS.length);
+ }
+
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion > newVersion) {
+ throw new RuntimeException("Downgrade of DB not supported");
+ }
+ for (int i = oldVersion; i < newVersion; i++) {
+ db.execSQL(MIGRATIONS[i]);
+ }
+ }
+
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ throw new RuntimeException("Downgrade of DB not supported");
+ }
+}
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/DBContract.java b/app/src/main/java/me/sergiotarxz/bedrockstation/DBContract.java
new file mode 100644
index 0000000..4ef1a4a
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/DBContract.java
@@ -0,0 +1,13 @@
+package me.sergiotarxz.bedrockstation;
+import android.provider.BaseColumns;
+
+public final class DBContract {
+ private DBContract() {}
+
+ public static class Options implements BaseColumns {
+ public static final String TABLE_NAME = "options";
+ public static final String COLUMN_NAME_ID = "id";
+ public static final String COLUMN_NAME_KEY = "key";
+ public static final String COLUMN_NAME_VALUE = "value";
+ }
+}
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/MainActivity.java b/app/src/main/java/me/sergiotarxz/bedrockstation/MainActivity.java
new file mode 100644
index 0000000..be932d8
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/MainActivity.java
@@ -0,0 +1,196 @@
+package me.sergiotarxz.bedrockstation;
+
+import android.os.Bundle;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import android.widget.LinearLayout;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.view.View;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.database.sqlite.SQLiteDatabase;
+import android.view.ViewGroup.LayoutParams;
+import android.content.Intent;
+import me.sergiotarxz.bedrockstation.ProxyService;
+import me.sergiotarxz.bedrockstation.Options;
+import me.sergiotarxz.bedrockstation.DB;
+import android.widget.Toast;
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
+import androidx.activity.result.ActivityResultLauncher;
+import android.app.NotificationManager;
+import android.content.ServiceConnection;
+import android.content.Context;
+import android.content.ComponentName;
+import android.os.IBinder;
+
+public class MainActivity extends AppCompatActivity {
+ interface Lambda {
+ void l();
+ };
+
+ ProxyService proxyService = null;
+ boolean mBound = false;
+ private Button button;
+ boolean serverStarted = false;
+ private EditText hostEditText = null;
+ private EditText portEditText = null;
+
+ private ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder service) {
+ ProxyService.LocalBinder binder = (ProxyService.LocalBinder) service;
+ proxyService = binder.getService();
+ if (proxyService.isServerStarted()) {
+ onStartProxyService();
+ } else {
+ onFinishProxyService();
+ }
+ proxyService.setActivity(MainActivity.this);
+ mBound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ mBound = false;
+ onFinishProxyService();
+ }
+ };
+
+ private void onChangeEditText(EditText edit, Lambda l) {
+ edit.addTextChangedListener(new TextWatcher() {
+
+ public void afterTextChanged(Editable s) {
+ l.l();
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ });
+ }
+
+
+ private void finishServiceProxy() {
+ Intent intent = new Intent(this, ProxyService.class);
+ intent.setAction(ProxyService.Action.END);
+ this.startForegroundService(intent);
+ }
+
+ private void startServiceProxy() {
+ Intent intent = new Intent(this, ProxyService.class);
+ this.startForegroundService(intent);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Intent intent = new Intent(this, ProxyService.class);
+ bindService(intent, connection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ unbindService(connection);
+
+ }
+
+ public void onStartProxyService() {
+ button.setText("Finish Proxy");
+ serverStarted = true;
+ }
+
+ public void onFinishProxyService() {
+ button.setText("Start Proxy");
+ serverStarted = false;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setLayoutParams(new LinearLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT));
+ layout.setGravity(Gravity.CENTER);
+ layout.setId(1);
+ SQLiteDatabase db = new DB(this).getInstance();
+ Options options = new Options(db);
+ button = new Button(this);
+ button.setOnClickListener( (View view) -> {
+ if(!serverStarted) {
+ if (!getSystemService(NotificationManager.class)
+ .areNotificationsEnabled()) {
+ requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
+ return;
+ }
+ startServiceProxy();
+ return;
+ }
+ finishServiceProxy();
+ });
+ if (serverStarted) {
+ button.setText("Finish Proxy");
+ } else {
+ button.setText("Start Proxy");
+ }
+ LayoutParams buttonLayoutParams = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ LayoutParams editTextLayoutParams = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ editTextLayoutParams.width = 500;
+ button.setLayoutParams(buttonLayoutParams);
+ LinearLayout hostLayout = new LinearLayout(this);
+ hostLayout.setGravity(Gravity.CENTER);
+ TextView hostIndicator = new TextView(this);
+ hostIndicator.setText("IP: ");
+ hostEditText = new EditText(this);
+ hostEditText.setText(options.get(Options.HOST_CACHE));
+ onChangeEditText(hostEditText, () -> {
+ String text = hostEditText.getText().toString();
+ options.set(Options.HOST_CACHE, text);
+ });
+ hostEditText.setLayoutParams(editTextLayoutParams);
+ hostLayout.addView(hostIndicator);
+ hostLayout.addView(hostEditText);
+ LinearLayout portLayout = new LinearLayout(this);
+ portLayout.setGravity(Gravity.CENTER);
+ TextView portIndicator = new TextView(this);
+ portEditText = new EditText(this);
+ onChangeEditText(portEditText, () -> {
+ String text = portEditText.getText().toString();
+ options.set(Options.PORT_CACHE, text);
+ });
+ portEditText.setLayoutParams(editTextLayoutParams);
+ portIndicator.setText("Port: ");
+ portEditText.setText(options.get(Options.PORT_CACHE));
+ portLayout.addView(portIndicator);
+ portLayout.addView(portEditText);
+ layout.addView(hostLayout);
+ layout.addView(portLayout);
+ layout.addView(button);
+ setContentView(layout);
+ }
+
+ private ActivityResultLauncher requestPermissionLauncher =
+ registerForActivityResult(new RequestPermission(), isGranted -> {
+ if (isGranted) {
+ startServiceProxy();
+ } else {
+ Toast.makeText(this, "You need notifications to run the proxy", Toast.LENGTH_LONG).show();
+ }
+ });
+
+}
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/Options.java b/app/src/main/java/me/sergiotarxz/bedrockstation/Options.java
new file mode 100644
index 0000000..e8fee0d
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/Options.java
@@ -0,0 +1,51 @@
+package me.sergiotarxz.bedrockstation;
+
+import me.sergiotarxz.bedrockstation.DBContract;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.content.ContentValues;
+
+public class Options {
+ public static final String PORT_CACHE = "port_cache";
+ public static final String HOST_CACHE = "host_cache";
+
+ private SQLiteDatabase db = null;
+ public Options(SQLiteDatabase db) {
+ this.db = db;
+ }
+
+ public String set(String key, String value) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(DBContract.Options.COLUMN_NAME_KEY, key);
+ contentValues.put(DBContract.Options.COLUMN_NAME_VALUE, value);
+ db.replace(DBContract.Options.TABLE_NAME, null, contentValues);
+ return value;
+ }
+
+ public String get(String key) {
+ String[] projection = {
+ DBContract.Options.COLUMN_NAME_VALUE
+ };
+ String selection = DBContract.Options.COLUMN_NAME_KEY + " = ?";
+ String[] selectionArgs = { key };
+ Cursor cursor = db.query(
+ DBContract.Options.TABLE_NAME,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ ""
+ );
+ if (!cursor.moveToNext()) {
+ return "";
+ }
+ String result = cursor.getString(
+ cursor.getColumnIndexOrThrow(
+ DBContract.Options.COLUMN_NAME_VALUE
+ )
+ );
+ cursor.close();
+ return result;
+ }
+}
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyService.java b/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyService.java
new file mode 100644
index 0000000..d0c3ed6
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyService.java
@@ -0,0 +1,155 @@
+package me.sergiotarxz.bedrockstation;
+
+import android.util.Log;
+import androidx.core.app.ServiceCompat;
+import androidx.core.app.NotificationCompat;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.Binder;
+import android.os.IBinder;
+import android.app.Service;
+import android.app.Notification;
+import android.content.Intent;
+import android.app.NotificationManager;
+import android.app.NotificationChannel;
+import java.net.InetSocketAddress;
+import android.app.PendingIntent;
+import android.database.sqlite.SQLiteDatabase;
+import me.sergiotarxz.bedrockstation.DB;
+import android.widget.Toast;
+import android.os.StrictMode;
+
+import me.sergiotarxz.bedrockstation.ProxyThread;
+
+public class ProxyService extends Service {
+
+ static String CHANNEL_ID = "sthaoes";
+ Thread proxyThread = null;
+
+ public class LocalBinder extends Binder {
+ ProxyService getService() {
+ return ProxyService.this;
+ }
+ }
+
+ private final IBinder binder = new LocalBinder();
+
+ private MainActivity activity = null;
+
+ public void setActivity(MainActivity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ static ProxyService instance = null;
+ static public ProxyService getInstance() {
+ return instance;
+ }
+
+ public class Action {
+ static final String START = "start";
+ static final String END = "end";
+ }
+
+ @Override
+ public void onCreate() {
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "BedrockProxy", NotificationManager.IMPORTANCE_DEFAULT);
+ notificationManager.createNotificationChannel(channel);
+ instance = this;
+ }
+
+ private void startForeground() {
+ try {
+ Intent intent = new Intent(this, ProxyService.class);
+ intent.setAction(Action.END);
+ PendingIntent pendingIntent = PendingIntent.getForegroundService(this, 100, intent, PendingIntent.FLAG_IMMUTABLE);
+ Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Bedrock Proxy is running")
+ .setContentText("Press this notification to kill")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .build();
+ int type = 0;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+ }
+ this.startForeground(
+ 1,
+ notification,
+ type
+ );
+ } catch (Exception e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent,
+ int flags,
+ int startId) {
+ if (intent.getAction() == null) {
+ intent.setAction(Action.START);
+ }
+ if (intent.getAction() == Action.START) {
+ startForeground();
+ startServer();
+ }
+ if (intent.getAction() == Action.END) {
+ Log.w("bedrockstation", "HOLA");
+ finishServer();
+ stopForeground(true);
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ public boolean isServerStarted() {
+ return proxyThread != null && proxyThread.isAlive();
+ }
+
+ public void finishServer() {
+ if (isServerStarted()) {
+ ((ProxyThread) proxyThread).terminate();
+ try {
+ proxyThread.join();
+ activity.onFinishProxyService();
+ } catch (Exception e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
+ }
+
+ private void createServer(InetSocketAddress address) {
+ try {
+ proxyThread = new ProxyThread(address);
+ } catch (Exception e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
+
+ public void startServer() {
+ if (!isServerStarted()) {
+ Options options = new Options(new DB(this).getInstance());
+ String host = options.get(Options.HOST_CACHE);
+
+ int port = 0;
+ try {
+ port = Integer.parseInt(options.get(Options.PORT_CACHE));
+ } catch (Exception e) {
+ Log.e("bedrockstation", Log.getStackTraceString(e));
+ Toast.makeText(this, "Port is not a number", Toast.LENGTH_LONG).show();
+ return;
+ }
+ StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+ StrictMode.setThreadPolicy(policy);
+ createServer(new InetSocketAddress(host, port));
+ proxyThread.start();
+ activity.onStartProxyService();
+ }
+ }
+}
diff --git a/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyThread.java b/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyThread.java
new file mode 100644
index 0000000..22d9372
--- /dev/null
+++ b/app/src/main/java/me/sergiotarxz/bedrockstation/ProxyThread.java
@@ -0,0 +1,133 @@
+package me.sergiotarxz.bedrockstation;
+
+import java.nio.channels.DatagramChannel;
+import java.nio.channels.Selector;
+import java.nio.channels.SelectableChannel;
+import java.util.IdentityHashMap;
+import java.util.HashMap;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.SelectionKey;
+import java.nio.ByteBuffer;
+import java.util.Set;
+import java.net.StandardSocketOptions;
+import java.util.Iterator;
+import java.lang.Thread;
+import android.util.Log;
+
+interface Lambda {
+ void l() throws Exception;
+};
+
+public class ProxyThread extends Thread
+{
+ IdentityHashMap