From f91f04c193a0038e9988db101429ee5481c83016 Mon Sep 17 00:00:00 2001 From: Alexey 'Cluster' Avdyukhin Date: Sun, 29 May 2022 16:35:29 +0300 Subject: First commit --- .idea/.gitignore | 3 + .idea/.name | 1 + .idea/compiler.xml | 6 + .idea/deploymentTargetDropDown.xml | 17 ++ .idea/misc.xml | 19 ++ .idea/render.experimental.xml | 6 + app/.gitignore | 1 + app/build.gradle | 41 ++++ app/proguard-rules.pro | 21 ++ .../ExampleInstrumentedTest.java | 26 +++ app/src/main/AndroidManifest.xml | 37 +++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 310749 bytes .../usbserialtelnetserver/MainActivity.java | 258 +++++++++++++++++++++ .../usbserialtelnetserver/TcpClientThread.java | 144 ++++++++++++ .../usbserialtelnetserver/TcpServerThread.java | 91 ++++++++ .../UsbSerialTelnetService.java | 241 +++++++++++++++++++ .../usbserialtelnetserver/UsbSerialThread.java | 60 +++++ app/src/main/res/color/text_color.xml | 5 + app/src/main/res/color/text_color_dark.xml | 5 + app/src/main/res/layout/activity_main.xml | 178 ++++++++++++++ app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 8071 bytes .../res/mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 48340 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2291 bytes app/src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 8782 bytes app/src/main/res/mipmap-hdpi/ic_notification.png | Bin 0 -> 2289 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 4468 bytes .../res/mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 23670 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1588 bytes app/src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 4692 bytes app/src/main/res/mipmap-mdpi/ic_notification.png | Bin 0 -> 1605 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 12680 bytes .../res/mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 81869 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2901 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 13843 bytes app/src/main/res/mipmap-xhdpi/ic_notification.png | Bin 0 -> 2722 bytes app/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 23919 bytes .../res/mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 181114 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 4647 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 25381 bytes app/src/main/res/mipmap-xxhdpi/ic_notification.png | Bin 0 -> 4141 bytes app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 37773 bytes .../res/mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 321990 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 6316 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 40140 bytes .../main/res/mipmap-xxxhdpi/ic_notification.png | Bin 0 -> 5278 bytes app/src/main/res/values-night/themes.xml | 18 ++ app/src/main/res/values/colors.xml | 13 ++ app/src/main/res/values/strings.xml | 22 ++ app/src/main/res/values/themes.xml | 18 ++ app/src/main/res/xml/usb_device_filter.xml | 26 +++ .../usbserialtelnetserver/ExampleUnitTest.java | 17 ++ build.gradle | 9 + gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 +++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle | 17 ++ 60 files changed, 1611 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/render.experimental.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/clusterrr/usbserialtelnetserver/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/clusterrr/usbserialtelnetserver/MainActivity.java create mode 100644 app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpClientThread.java create mode 100644 app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpServerThread.java create mode 100644 app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialTelnetService.java create mode 100644 app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialThread.java create mode 100644 app/src/main/res/color/text_color.xml create mode 100644 app/src/main/res/color/text_color_dark.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_background.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_notification.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_background.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_notification.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_background.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_notification.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_notification.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_notification.png create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/usb_device_filter.xml create mode 100644 app/src/test/java/com/clusterrr/usbserialtelnetserver/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..d18fb5d --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +USB Serial Telnet Server \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..8f6e4e0 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a0f7380 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..89583e7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.clusterrr.usbserialtelnetserver" + minSdk 26 + targetSdk 32 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + applicationVariants.all { variant -> + variant.outputs.all { output -> + def formattedDate = new Date().format('yyMMdd-HHmmss') + outputFileName = "UsbSerialTelnetServer-v${defaultConfig.versionCode}-${formattedDate}.apk" + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.6.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'com.github.mik3y:usb-serial-for-android:3.4.3' +} 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/androidTest/java/com/clusterrr/usbserialtelnetserver/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/clusterrr/usbserialtelnetserver/ExampleInstrumentedTest.java new file mode 100644 index 0000000..72b7ade --- /dev/null +++ b/app/src/androidTest/java/com/clusterrr/usbserialtelnetserver/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.clusterrr.usbserialtelnetserver; + +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("com.clusterrr.usbserialtelnetserver", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3dc5fa2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..1ca72ee Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/clusterrr/usbserialtelnetserver/MainActivity.java b/app/src/main/java/com/clusterrr/usbserialtelnetserver/MainActivity.java new file mode 100644 index 0000000..0206003 --- /dev/null +++ b/app/src/main/java/com/clusterrr/usbserialtelnetserver/MainActivity.java @@ -0,0 +1,258 @@ +package com.clusterrr.usbserialtelnetserver; + +import static android.hardware.usb.UsbManager.EXTRA_PERMISSION_GRANTED; +import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import com.hoho.android.usbserial.driver.UsbSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialPort; +import com.hoho.android.usbserial.driver.UsbSerialProber; + +import java.text.ParseException; +import java.util.List; + +public class MainActivity extends AppCompatActivity implements View.OnClickListener, UsbSerialTelnetService.IOnStopListener { + final static String SETTING_TCP_PORT = "tcp_port"; + final static String SETTING_BAUD_RATE = "baud_rate"; + final static String SETTING_DATA_BITS = "data_bits"; + final static String SETTING_STOP_BITS = "stop_bits"; + final static String SETTING_PARITY = "parity"; + + UsbSerialTelnetService.ServiceBinder mServiceBinder = null; + Button mStartButton; + Button mStopButton; + EditText mTcpPort; + EditText mBaudRate; + Spinner mDataBits; + Spinner mStopBits; + Spinner mParity; + TextView mStatus; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + mStartButton = findViewById(R.id.buttonStart); + mStopButton = findViewById(R.id.buttonStop); + mTcpPort = findViewById(R.id.editTextTcpPort); + mBaudRate = findViewById(R.id.editTextNumberBaudRate); + mDataBits = findViewById(R.id.spinnerDataBits); + mStopBits = findViewById(R.id.spinnerStopBits); + mParity = findViewById(R.id.spinnerParity); + mStatus = findViewById(R.id.textViewStatus); + + mStartButton.setOnClickListener(this); + mStopButton.setOnClickListener(this); + + Intent serviceIntent = new Intent(this, UsbSerialTelnetService.class); + bindService(serviceIntent, serviceConnection, 0); // in case it's service already started + Intent intent = getIntent(); + //if (intent != null) onNewIntent(intent); + + updateSettings(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (intent != null) { + if (intent.getBooleanExtra(UsbSerialTelnetService.KEY_NEED_TO_START, false)) { + // Test that permission is granted + UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); + List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); + if (!availableDrivers.isEmpty()) { + UsbSerialDriver driver = availableDrivers.get(0); + UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); + if (connection != null) start(); + } + } + } + } + + @Override + protected void onPause() { + super.onPause(); + saveSettings(); + } + + @Override + public void onClick(View view) + { + switch(view.getId()) + { + case R.id.buttonStart: + start(); + break; + case R.id.buttonStop: + stop(); + break; + } + } + + private void start() { + saveSettings(); + + Intent ignoreOptimization = prepareIntentForWhiteListingOfBatteryOptimization( + this, getPackageName(), false); + if (ignoreOptimization != null) startActivity(ignoreOptimization); + + Intent serviceIntent = new Intent(this, UsbSerialTelnetService.class); + SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); + serviceIntent.putExtra(UsbSerialTelnetService.KEY_TCP_PORT, prefs.getInt(SETTING_TCP_PORT, 2323)); + serviceIntent.putExtra(UsbSerialTelnetService.KEY_BAUD_RATE, prefs.getInt(SETTING_BAUD_RATE, 115200)); + serviceIntent.putExtra(UsbSerialTelnetService.KEY_DATA_BITS, prefs.getInt(SETTING_DATA_BITS, 3) + 5); + switch (prefs.getInt(SETTING_STOP_BITS, 0)) { + case 0: + serviceIntent.putExtra(UsbSerialTelnetService.KEY_STOP_BITS, UsbSerialPort.STOPBITS_1); + break; + case 1: + serviceIntent.putExtra(UsbSerialTelnetService.KEY_STOP_BITS, UsbSerialPort.STOPBITS_1_5); + break; + case 2: + serviceIntent.putExtra(UsbSerialTelnetService.KEY_STOP_BITS, UsbSerialPort.STOPBITS_2); + break; + } + serviceIntent.putExtra(UsbSerialTelnetService.KEY_PARITY, prefs.getInt(SETTING_PARITY, 0)); + startForegroundService(serviceIntent); + bindService(serviceIntent, serviceConnection, 0); + } + + private void stop() { + Intent serviceIntent = new Intent(this, UsbSerialTelnetService.class); + stopService(serviceIntent); + mServiceBinder = null; + updateSettings(); + } + + private ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + mServiceBinder = (UsbSerialTelnetService.ServiceBinder) service; + mServiceBinder.setOnStopListener(MainActivity.this); + updateSettings(); + Log.d(UsbSerialTelnetService.TAG, "Service connected"); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mServiceBinder = null; + Log.d(UsbSerialTelnetService.TAG, "Service disconnected"); + } + }; + + @Override + public void usbSerialServiceStopped() { + updateSettings(); + } + private void saveSettings() { + SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); + int tcpPort; + try { + tcpPort = Integer.parseInt(mTcpPort.getText().toString()); + } + catch (NumberFormatException e) { + tcpPort = 2323; + } + int baudRate; + try { + baudRate = Integer.parseInt(mBaudRate.getText().toString()); + } + catch (NumberFormatException e) { + baudRate = 115200; + } + prefs.edit() + .putInt(SETTING_TCP_PORT, tcpPort) + .putInt(SETTING_BAUD_RATE, baudRate) + .putInt(SETTING_DATA_BITS, mDataBits.getSelectedItemPosition()) + .putInt(SETTING_STOP_BITS, mStopBits.getSelectedItemPosition()) + .putInt(SETTING_PARITY, mParity.getSelectedItemPosition()) + .commit(); + } + + private void updateSettings() { + SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); + boolean started = mServiceBinder != null && mServiceBinder.isStarted(); + mStartButton.setEnabled(!started); + mStopButton.setEnabled(started); + mTcpPort.setEnabled(!started); + mBaudRate.setEnabled(!started); + mDataBits.setEnabled(!started); + mStopBits.setEnabled(!started); + mParity.setEnabled(!started); + mTcpPort.setText(String.valueOf(prefs.getInt(SETTING_TCP_PORT, 2323))); + mBaudRate.setText(String.valueOf(prefs.getInt(SETTING_BAUD_RATE, 115200))); + mDataBits.setSelection(prefs.getInt(SETTING_DATA_BITS, 3)); + mStopBits.setSelection(prefs.getInt(SETTING_STOP_BITS, 0)); + mParity.setSelection(prefs.getInt(SETTING_PARITY, 0)); + if (started) + mStatus.setText("Started, please connect to: telnet://" + UsbSerialTelnetService.getIPAddress() + ": "+ mTcpPort.getText()); + else + mStatus.setText("Not started"); + } + + public static Intent prepareIntentForWhiteListingOfBatteryOptimization(Context context, String packageName, boolean alsoWhenWhiteListed) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return null; + if (ContextCompat.checkSelfPermission(context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED) + return null; + final WhiteListedInBatteryOptimizations appIsWhiteListedFromPowerSave = getIfAppIsWhiteListedFromBatteryOptimizations(context, packageName); + Intent intent = null; + switch (appIsWhiteListedFromPowerSave) { + case WHITE_LISTED: + if (alsoWhenWhiteListed) + intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + break; + case NOT_WHITE_LISTED: + intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).setData(Uri.parse("package:" + packageName)); + break; + case ERROR_GETTING_STATE: + case UNKNOWN_TOO_OLD_ANDROID_API_FOR_CHECKING: + case IRRELEVANT_OLD_ANDROID_API: + default: + break; + } + return intent; + } + + public enum WhiteListedInBatteryOptimizations { + WHITE_LISTED, NOT_WHITE_LISTED, ERROR_GETTING_STATE, UNKNOWN_TOO_OLD_ANDROID_API_FOR_CHECKING, IRRELEVANT_OLD_ANDROID_API + } + + public static WhiteListedInBatteryOptimizations getIfAppIsWhiteListedFromBatteryOptimizations(Context context, String packageName) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return WhiteListedInBatteryOptimizations.IRRELEVANT_OLD_ANDROID_API; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return WhiteListedInBatteryOptimizations.UNKNOWN_TOO_OLD_ANDROID_API_FOR_CHECKING; + final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm == null) + return WhiteListedInBatteryOptimizations.ERROR_GETTING_STATE; + return pm.isIgnoringBatteryOptimizations(packageName) ? WhiteListedInBatteryOptimizations.WHITE_LISTED : WhiteListedInBatteryOptimizations.NOT_WHITE_LISTED; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpClientThread.java b/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpClientThread.java new file mode 100644 index 0000000..3e793ab --- /dev/null +++ b/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpClientThread.java @@ -0,0 +1,144 @@ +package com.clusterrr.usbserialtelnetserver; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; + +public class TcpClientThread extends Thread { + private UsbSerialTelnetService mUsbSerialTelnetService; + private TcpServerThread mTcpServerThread; + private Socket mSocket; + private InputStream mDataInputStream; + private OutputStream mDataOutputStream; + private String mAddress; + private List mBuffer; + + public TcpClientThread(UsbSerialTelnetService usbSerialTelnetService, TcpServerThread tcpServerThread, Socket socket) throws IOException { + mUsbSerialTelnetService = usbSerialTelnetService; + mTcpServerThread = tcpServerThread; + mSocket = socket; + mDataInputStream = mSocket.getInputStream(); + mDataOutputStream = mSocket.getOutputStream(); + mAddress = mSocket.getRemoteSocketAddress().toString(); + mBuffer = new ArrayList<>(); + } + + @Override + public void run() { + byte buffer[] = new byte[1024]; + + try { + mDataOutputStream.write(new byte[]{(byte) 0xFF, (byte) 0xFD, (byte) 0x03}); // Do Suppress Go Ahead + mDataOutputStream.write(new byte[]{(byte) 0xFF, (byte) 0xFB, (byte) 0x03}); // Will Suppress Go Ahead + mDataOutputStream.write(new byte[]{(byte) 0xFF, (byte) 0xFB, (byte) 0x01}); // Will Echo + while (true) { + if (mDataInputStream == null) break; + int l = mDataInputStream.read(buffer); + if (l <= 0) break; // disconnect + for (int i = 0; i < l; i++) + mBuffer.add(buffer[i]); + proceedBuffer(); + } + } + catch (SocketException e) { + Log.i(UsbSerialTelnetService.TAG, mAddress + ": " + e.getMessage()); + } + catch (IOException e) { + e.printStackTrace(); + } + + try { + if (mSocket != null) + mSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + mSocket = null; + mDataInputStream = null; + mDataOutputStream = null; + mTcpServerThread.removeClient(this); + Log.i(UsbSerialTelnetService.TAG, mAddress + ": stopped"); + } + + private void proceedBuffer() throws IOException { + int len = mBuffer.size(); + int i = 0; + byte[] output = new byte[len]; + int outputSize = 0; + for (; i < len; i++) { + byte b = mBuffer.get(i); + if (b == 0) continue; + if (b == '\n') continue; + /* + if ((b == '\r') && ((i >= len) || (mBuffer.get(i + 1) == '\n'))) { + // skip \r\n + if (true) + continue; + } + */ + if (b == (byte)0xFF) { + if (i >= len) break; + byte next = mBuffer.get(i + 1); + if (next == (byte)0xFF) { + // just 0xFF + //mUsbSerialTelnetService.writePort((byte) 0xFF); + output[outputSize++] = (byte)0xFF; + i++; + continue; + } + // Command + if (i + 1 >= len) break; + byte cmd = next; + byte opt = mBuffer.get(i + 2); + Log.d(UsbSerialTelnetService.TAG, "Telnet command: CMD=" + Integer.toHexString(cmd >= 0 ? cmd : cmd + 256) + " ARG=" + Integer.toHexString(opt >= 0 ? opt : opt + 256)); + i += 2; + continue; + } + // just data + //mUsbSerialTelnetService.writePort(b); + output[outputSize++] = b; + } + + // Remove proceeded + for (int j = 0; j < i; j++) + mBuffer.remove(0); + + mUsbSerialTelnetService.writeSerialPort(output, 0, outputSize); + } + + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + public void write(byte[] data, int offset, int len) throws IOException { + if (mDataOutputStream != null) + mDataOutputStream.write(data, offset, len); + //Log.d(UsbSerialTelnetService.TAG, "Writing " + len + " bytes to TCP"); + } + + public void close() { + try { + if (mSocket != null) { + mSocket.close(); + mSocket = null; + } + if (mDataOutputStream != null) { + mDataOutputStream.close(); + mDataOutputStream = null; + } + if (mDataInputStream != null) + { + mDataInputStream.close(); + mDataInputStream = null; + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpServerThread.java b/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpServerThread.java new file mode 100644 index 0000000..b18c516 --- /dev/null +++ b/app/src/main/java/com/clusterrr/usbserialtelnetserver/TcpServerThread.java @@ -0,0 +1,91 @@ +package com.clusterrr.usbserialtelnetserver; + +import android.util.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; + +public class TcpServerThread extends Thread { + private UsbSerialTelnetService mUsbSerialTelnetService; + private ServerSocket mTcpServer; + private List mClients; + + public TcpServerThread(UsbSerialTelnetService usbSerialTelnetService, ServerSocket tcpServer) + { + mUsbSerialTelnetService = usbSerialTelnetService; + mTcpServer = tcpServer; + mClients = new ArrayList<>(); + } + + @Override + public void run() { + try { + while (true) { + if (mTcpServer == null) break; + Socket socket = mTcpServer.accept(); + Log.i(UsbSerialTelnetService.TAG, "Connected: " + socket.getRemoteSocketAddress()); + TcpClientThread client = new TcpClientThread(mUsbSerialTelnetService, this, socket); + client.start(); + mClients.add(client); + } + } + catch (SocketException e) { + Log.i(UsbSerialTelnetService.TAG, "Server: " + e.getMessage()); + } + catch (Exception e) { + e.printStackTrace(); + } + close(); + Log.i(UsbSerialTelnetService.TAG, "Server: stopped"); + } + + public void removeClient(TcpClientThread tcpClientThread) { + mClients.remove(tcpClientThread); + } + + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + public void write(byte[] data, int offset, int len) throws IOException { + List toRemove = new ArrayList<>(); + for (TcpClientThread client : mClients) { + try { + client.write(data, offset, len); + } + catch (Exception ex) { + ex.printStackTrace(); + toRemove.add(client); + } + } + for (TcpClientThread client : toRemove) { + client.close(); + mClients.remove(client); + } + } + + public void close() + { + try { + if (mTcpServer != null) { + mTcpServer.close(); + mTcpServer = null; + } + } catch (IOException e) { + e.printStackTrace(); + } + for (TcpClientThread client : mClients) { + try { + client.close(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + mClients.clear(); + } +} diff --git a/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialTelnetService.java b/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialTelnetService.java new file mode 100644 index 0000000..eb0f292 --- /dev/null +++ b/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialTelnetService.java @@ -0,0 +1,241 @@ +package com.clusterrr.usbserialtelnetserver; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.app.NotificationCompat; + +import com.hoho.android.usbserial.driver.UsbSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialPort; +import com.hoho.android.usbserial.driver.UsbSerialProber; +import com.hoho.android.usbserial.util.SerialInputOutputManager; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.List; + +public class UsbSerialTelnetService extends Service { + final static String TAG = "UsbSerialTelnet"; + final static String KEY_NEED_TO_START = "need_to_start"; +// final static String START_INTENT = "com.clusterrr.usbserialtelnetserver.START"; +// final static String STOP_INTENT = "com.clusterrr.usbserialtelnetserver.STOP"; +// final static String STARTED_INTENT = "com.clusterrr.usbserialtelnetserver.STARTED"; +// final static String STOPPED_INTENT = "com.clusterrr.usbserialtelnetserver.STOPPED"; + final static String KEY_TCP_PORT = "tcp_port"; + final static String KEY_BAUD_RATE = "baud_rate"; + final static String KEY_DATA_BITS = "data_bits"; + final static String KEY_STOP_BITS = "stop_bits"; + final static String KEY_PARITY = "parity"; + + boolean mStarted = false; + //UsbSerialPort mSerialPort = null; + UsbSerialThread mUsbSerialThread = null; + TcpServerThread mTcpServerThread = null; + + int mTcpPort = 2323; + + public UsbSerialTelnetService() { + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + if (mStarted) { + // Already started + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(UsbSerialTelnetService.this.getApplicationContext(), "Already started", Toast.LENGTH_LONG).show()); + return START_STICKY; + } + + String message = getString(R.string.app_name) + " started"; + boolean success = false; + + try { + // Find all available drivers from attached devices. + UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); + List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); + if (availableDrivers.isEmpty()) { + message = "Error: USB serial device not found"; + } else { + // Open a connection to the first available driver. + UsbSerialDriver driver = availableDrivers.get(0); + UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); + if (connection == null) { + message = null; // "Please grant permission and try again"; + Intent mainActivityStartIntent = new Intent(this, MainActivity.class); + mainActivityStartIntent.putExtra(KEY_NEED_TO_START, true); + mainActivityStartIntent.setAction(KEY_NEED_TO_START); + PendingIntent mainActivityStartPendingIntent = PendingIntent.getActivity(this, 0, mainActivityStartIntent, PendingIntent.FLAG_IMMUTABLE); + manager.requestPermission(driver.getDevice(), mainActivityStartPendingIntent); + } else { + UsbSerialPort serialPort = driver.getPorts().get(0); // Most devices have just one port (port 0) + serialPort.open(connection); + serialPort.setParameters( + intent.getIntExtra(KEY_BAUD_RATE, 115200), + intent.getIntExtra(KEY_DATA_BITS, 8), + intent.getIntExtra(KEY_STOP_BITS, UsbSerialPort.STOPBITS_1), + intent.getIntExtra(KEY_PARITY, UsbSerialPort.PARITY_NONE)); + ServerSocket serverSocket = new ServerSocket(intent.getIntExtra(KEY_TCP_PORT,2323)); + mUsbSerialThread = new UsbSerialThread(this, serialPort); + mTcpServerThread = new TcpServerThread(this, serverSocket); + mUsbSerialThread.start(); + mTcpServerThread.start(); + success = true; + } + } + } + catch (Exception ex) { + message = "Error: " + ex.getMessage(); + ex.printStackTrace(); + } + + Intent mainActivityIntent = new Intent(this, MainActivity.class); + PendingIntent mainActivityPendingIntent = PendingIntent.getActivity(this, 0, mainActivityIntent, PendingIntent.FLAG_IMMUTABLE); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(TAG, + getString(R.string.app_name), + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(getString(R.string.app_name)); + nm.createNotificationChannel(channel); + } + Notification notification = new NotificationCompat.Builder(this, TAG) + .setOngoing(true) + .setSmallIcon(R.mipmap.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) + .setContentTitle(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(Notification.CATEGORY_SERVICE) + .setShowWhen(false) + .setContentIntent(mainActivityPendingIntent) + .build(); + + if (message != null) { + final String msg = message; + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(UsbSerialTelnetService.this.getApplicationContext(), msg, Toast.LENGTH_SHORT).show()); + } + + startForeground(1, notification); + + if (success) { + if (message != null) + Log.i(TAG, message); + Log.d(TAG, String.format("Local IP Address: %s", getIPAddress())); + mStarted = true; + } else { + if (message != null) + Log.e(TAG, message); + stopSelf(); + mStarted = false; + } + return START_STICKY; + } + + public static String getIPAddress() { + try { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + if (networkInterface.isUp()) { + String name = networkInterface.getName(); + if (name.toLowerCase().equals("wlan0") || name.toLowerCase().equals("rmnet0")) { + List interfaceAddresses = networkInterface.getInterfaceAddresses(); + for (InterfaceAddress interfaceAddress : interfaceAddresses) { + InetAddress address = interfaceAddress.getAddress(); + if (address instanceof Inet4Address){ + return address.getHostAddress(); + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return "localhost"; + } + + @Override + public void onDestroy() + { + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(1); + if (mTcpServerThread != null) { + mTcpServerThread.close(); + mTcpServerThread = null; + } + if (mUsbSerialThread != null) { + mUsbSerialThread.close(); + mUsbSerialThread = null; + } + if (mStarted) + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(UsbSerialTelnetService.this.getApplicationContext(), getString(R.string.app_name) + " stopped", Toast.LENGTH_SHORT).show()); + Log.i(TAG, "Service stopped"); + mStarted = false; + mBinder.stopped(); + } + + private final ServiceBinder mBinder = new ServiceBinder(); + public class ServiceBinder extends Binder { + private IOnStopListener onStopListener = null; + public boolean isStarted() + { + return mStarted; + } + public void setOnStopListener(IOnStopListener listener) { onStopListener = listener; } + public void stopped() { if (onStopListener != null) onStopListener.usbSerialServiceStopped(); } + } + public interface IOnStopListener + { + public void usbSerialServiceStopped(); + } + + public void writeSerialPort(byte[] buffer) throws IOException { + if (mUsbSerialThread == null) return; + mUsbSerialThread.write(buffer); + } + + public void writeSerialPort(byte[] buffer, int pos, int len) throws IOException { + if (mUsbSerialThread == null) return; + byte[] writeBuffer = new byte[len]; + System.arraycopy(buffer, pos, writeBuffer, 0, len); + mUsbSerialThread.write(writeBuffer); + } + + public void writeClients(byte[] buffer) throws IOException { + if (mTcpServerThread == null) return; + mTcpServerThread.write(buffer); + } + + public void writeClients(byte[] buffer, int pos, int len) throws IOException { + if (mTcpServerThread == null) return; + mTcpServerThread.write(buffer, pos, len); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialThread.java b/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialThread.java new file mode 100644 index 0000000..6bfe4e6 --- /dev/null +++ b/app/src/main/java/com/clusterrr/usbserialtelnetserver/UsbSerialThread.java @@ -0,0 +1,60 @@ +package com.clusterrr.usbserialtelnetserver; + +import android.util.Log; + +import com.hoho.android.usbserial.driver.UsbSerialPort; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.SocketException; + +public class UsbSerialThread extends Thread { + final static int WRITE_TIMEOUT = 1000; + + private UsbSerialTelnetService mUsbSerialTelnetService; + private UsbSerialPort mSerialPort; + + public UsbSerialThread(UsbSerialTelnetService usbSerialTelnetService, UsbSerialPort serialPort) { + mUsbSerialTelnetService = usbSerialTelnetService; + mSerialPort = serialPort; + } + + @Override + public void run() { + byte buffer[] = new byte[1024]; + + try { + while (true) { + if (mSerialPort == null) break; + int l = mSerialPort.read(buffer, 0); + if (l <= 0) break; // disconnect + mUsbSerialTelnetService.writeClients(buffer, 0, l); + } + } + catch (IOException e) { + Log.i(UsbSerialTelnetService.TAG, "Serial port: " + e.getMessage()); + } + catch (Exception e) { + e.printStackTrace(); + } + close(); + Log.i(UsbSerialTelnetService.TAG, "Serial port closed"); + mUsbSerialTelnetService.stopSelf(); + } + + public void write(byte[] data) throws IOException { + if (mSerialPort != null) + mSerialPort.write(data, WRITE_TIMEOUT); + } + + public void close() { + try { + if (mSerialPort != null) { + mSerialPort.close(); + mSerialPort = null; + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/color/text_color.xml b/app/src/main/res/color/text_color.xml new file mode 100644 index 0000000..1cf3b79 --- /dev/null +++ b/app/src/main/res/color/text_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/text_color_dark.xml b/app/src/main/res/color/text_color_dark.xml new file mode 100644 index 0000000..922c96a --- /dev/null +++ b/app/src/main/res/color/text_color_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..20ffbb6 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,178 @@ + + + +