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 objectToFunc; + HashMap addressToServer; + Selector selector; + DatagramChannel server; + InetSocketAddress remoteServerAddress; + + + private volatile boolean running = true; + + public void terminate() { + running = false; + } + + public ProxyThread(InetSocketAddress remoteServerAddress) throws Exception { + this.remoteServerAddress = remoteServerAddress; + objectToFunc = new IdentityHashMap(); + addressToServer = new HashMap(); + selector = Selector.open(); + } + + @Override + public void run() { + try { + server = createServer(); + while (running) { + selector.select(1000); + Set selectedKeys = selector.selectedKeys(); + Iterator iter = selectedKeys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + SelectableChannel channel = key.channel(); + if (!(channel instanceof DatagramChannel)) { + iter.remove(); + continue; + } + DatagramChannel dchannel = (DatagramChannel) channel; + if (key.isReadable()) { + objectToFunc.get(dchannel).l(); + } + iter.remove(); + } + } + server.close(); + } catch (Exception e) { + throw new RuntimeException(Log.getStackTraceString(e)); + } + } + + public DatagramChannel createServer() throws Exception { + DatagramChannel datagramChannel = DatagramChannel.open(); + InetSocketAddress serverAddress = new InetSocketAddress("0.0.0.0", remoteServerAddress.getPort()); + datagramChannel.bind(serverAddress); + datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true); + datagramChannel.configureBlocking(false); + objectToFunc.put(datagramChannel, () -> { + onServerCanRead(datagramChannel); + }); + datagramChannel.register(selector, SelectionKey.OP_READ); + System.out.println("Redirecting UDP connections from (" + "0.0.0.0" + ":" + remoteServerAddress.getPort() + ") to (" + remoteServerAddress.getHostString() + ":" + remoteServerAddress.getPort() + ")"); + return datagramChannel; + } + + public DatagramChannel createClientFinalServer(SocketAddress finalClientAddress) throws Exception { + DatagramChannel datagramChannel = DatagramChannel.open(); + InetSocketAddress serverAddress = remoteServerAddress; + datagramChannel.bind(null); + datagramChannel.configureBlocking(false); + objectToFunc.put(datagramChannel, () -> { + onClientFinalServerCanRead(datagramChannel, finalClientAddress); + }); + datagramChannel.register(selector, SelectionKey.OP_READ); + return datagramChannel; + } + + public void onClientFinalServerCanRead( + DatagramChannel finalServerConnection, + SocketAddress finalClientAddress + ) throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(1024); + finalServerConnection.receive(buffer); + byte[] array = buffer.array(); + int size = buffer.capacity() - (buffer.capacity() - buffer.position()); + server.send(ByteBuffer.wrap(array, 0, size), finalClientAddress); + } + + public void onServerCanRead( + DatagramChannel server + ) throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(1024); + SocketAddress clientAddress = server.receive(buffer); + int size = buffer.capacity() - (buffer.capacity() - buffer.position()); + byte[] array = buffer.array(); + getClientFinalConnection(clientAddress) + .send(ByteBuffer.wrap(array, 0, size), new InetSocketAddress("192.168.2.1", 19132)); + } + + public DatagramChannel getClientFinalConnection( SocketAddress clientAddress ) throws Exception { + DatagramChannel connection = addressToServer.get(clientAddress.toString()); + if (connection != null) { + return connection; + } + connection = createClientFinalServer(clientAddress); + if (clientAddress instanceof InetSocketAddress) { + InetSocketAddress clientAddressInet = (InetSocketAddress) clientAddress; + System.out.println("New UDP Client at (" + clientAddressInet.getHostString() + ":" + clientAddressInet.getPort() + ")"); + } + addressToServer.put(clientAddress.toString(), connection); + return connection; + } +} diff --git a/app/src/main/res/.exists b/app/src/main/res/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/drawable/.exists b/app/src/main/res/drawable/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/.exists b/app/src/main/res/layout/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..86a5d97 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/.exists b/app/src/main/res/mipmap-anydpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/.exists b/app/src/main/res/mipmap-hdpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/.exists b/app/src/main/res/mipmap-mdpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/.exists b/app/src/main/res/mipmap-xhdpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/.exists b/app/src/main/res/mipmap-xxhdpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/.exists b/app/src/main/res/mipmap-xxxhdpi/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/.exists b/app/src/main/res/values-night/.exists new file mode 100644 index 0000000..e69de29 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..a13a665 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/.exists b/app/src/main/res/values/.exists new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d64c4c2 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BedrockStation + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1961085 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +