Commit 7e03c31e by Sergio Valero

Initial commit

parent 7efeb0dd
*.iml
.gradle
/.gradle/
/.idea/
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/app/build/
/build/
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$PROJECT_DIR$/../../git_lab_cimne_upc_edu/OkoCharger" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/Proyectos/Example/SimpleUsbTerminal/app/src/main/res/layout/activity_main.xml" value="0.20520833333333333" />
<entry key="..\:/Proyectos/Example/SimpleUsbTerminal/app/src/main/res/layout/activity_main2.xml" value="0.2957427536231884" />
<entry key="..\:/Proyectos/Example/SimpleUsbTerminal/app/src/main/res/layout/device_list_header.xml" value="0.20520833333333333" />
<entry key="..\:/Proyectos/Example/SimpleUsbTerminal/app/src/main/res/layout/device_list_item.xml" value="0.20520833333333333" />
<entry key="..\:/Proyectos/Example/SimpleUsbTerminal/app/src/main/res/layout/fragment_terminal.xml" value="0.20520833333333333" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
......
MIT License
Copyright (c) 2019 Kai Morich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/83070da7805b4899820e285d2f7847b9)](https://www.codacy.com/manual/kai-morich/SimpleUsbTerminal?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=kai-morich/SimpleUsbTerminal&amp;utm_campaign=Badge_Grade)
# SimpleUsbTerminal
This Android app provides a line-oriented terminal / console for devices with a serial / UART interface connected with a USB-to-serial-converter.
It supports USB to serial converters based on
- FTDI FT232, FT2232, ...
- Prolific PL2303
- Silabs CP2102, CP2105, ...
- Qinheng CH340, CH341
and devices implementing the USB CDC protocol like
- Arduino using ATmega32U4
- Digispark using V-USB software USB
- BBC micro:bit using ARM mbed DAPLink firmware
## Features
- permission handling on device connection
- foreground service to buffer receive data while the app is rotating, in background, ...
## Credits
The app uses the [usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android) library.
## Motivation
I got various requests asking for help with Android development or source code for my
[Serial USB Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_usb_terminal) app.
Here you find a simplified version of my app.
......@@ -3,36 +3,33 @@ plugins {
}
android {
compileSdk 30
compileSdkVersion 30
defaultConfig {
applicationId "com.cimne.tic.oko.frame.connectusbserial"
minSdk 23
targetSdk 30
targetSdkVersion 30
minSdkVersion 18
vectorDrawables.useSupportLibrary true
applicationId "de.kai_morich.simple_usb_terminal"
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.github.mik3y:usb-serial-for-android:3.4.3' // maven jitpack
//implementation('com.github.mik3y:usb-serial-for-android:3.0.0beta') { changing = true } // maven local
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cimne.tic.oko.frame.connectusbserial">
xmlns:tools="http://schemas.android.com/tools"
package="de.kai_morich.simple_usb_terminal">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ConnectUSBSerial">
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="false" />
<!--
for this simple app launchMode=singleTask and singleTop have same effect.
If you would start another activity in the app, e.g. Android Settings
then you should use singleTask, else a new MainActivity would be started
when the settings activity is currently shown
-->
<activity
android:name=".MainActivity2"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/usb_device_filter" />
</activity>
<service android:name=".SerialService" />
</application>
</manifest>
\ No newline at end of file
package de.kai_morich.simple_usb_terminal;
class Constants {
// values have to be globally unique
static final String INTENT_ACTION_GRANT_USB = BuildConfig.APPLICATION_ID + ".GRANT_USB";
static final String INTENT_ACTION_DISCONNECT = BuildConfig.APPLICATION_ID + ".Disconnect";
static final String NOTIFICATION_CHANNEL = BuildConfig.APPLICATION_ID + ".Channel";
static final String INTENT_CLASS_MAIN_ACTIVITY = BuildConfig.APPLICATION_ID + ".MainActivity";
// values have to be unique within each app
static final int NOTIFY_MANAGER_START_FOREGROUND_SERVICE = 1001;
private Constants() {}
}
package de.kai_morich.simple_usb_terminal;
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver;
import com.hoho.android.usbserial.driver.ProbeTable;
import com.hoho.android.usbserial.driver.UsbSerialProber;
/**
* add devices here, that are not known to DefaultProber
*
* if the App should auto start for these devices, also
* add IDs to app/src/main/res/xml/usb_device_filter.xml
*/
class CustomProber {
static UsbSerialProber getCustomProber() {
ProbeTable customTable = new ProbeTable();
customTable.addProduct(0x16d0, 0x087e, CdcAcmSerialDriver.class); // e.g. Digispark CDC
return new UsbSerialProber(customTable);
}
}
package de.kai_morich.simple_usb_terminal;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.ListFragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import java.util.ArrayList;
import java.util.Locale;
public class DevicesFragment extends ListFragment {
static class ListItem {
UsbDevice device;
int port;
UsbSerialDriver driver;
ListItem(UsbDevice device, int port, UsbSerialDriver driver) {
this.device = device;
this.port = port;
this.driver = driver;
}
}
private final ArrayList<ListItem> listItems = new ArrayList<>();
private ArrayAdapter<ListItem> listAdapter;
private int baudRate = 19200;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
listAdapter = new ArrayAdapter<ListItem>(getActivity(), 0, listItems) {
@NonNull
@Override
public View getView(int position, View view, @NonNull ViewGroup parent) {
ListItem item = listItems.get(position);
if (view == null)
view = getActivity().getLayoutInflater().inflate(R.layout.device_list_item, parent, false);
TextView text1 = view.findViewById(R.id.text1);
TextView text2 = view.findViewById(R.id.text2);
if(item.driver == null)
text1.setText("<no driver>");
else if(item.driver.getPorts().size() == 1)
text1.setText(item.driver.getClass().getSimpleName().replace("SerialDriver",""));
else
text1.setText(item.driver.getClass().getSimpleName().replace("SerialDriver","")+", Port "+item.port);
text2.setText(String.format(Locale.US, "Vendor %04X, Product %04X", item.device.getVendorId(), item.device.getProductId()));
return view;
}
};
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setListAdapter(null);
View header = getActivity().getLayoutInflater().inflate(R.layout.device_list_header, null, false);
getListView().addHeaderView(header, null, false);
setEmptyText("<no USB devices found>");
((TextView) getListView().getEmptyView()).setTextSize(18);
setListAdapter(listAdapter);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_devices, menu);
}
@Override
public void onResume() {
super.onResume();
refresh();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if(id == R.id.refresh) {
refresh();
return true;
} else if (id ==R.id.baud_rate) {
final String[] baudRates = getResources().getStringArray(R.array.baud_rates);
int pos = java.util.Arrays.asList(baudRates).indexOf(String.valueOf(baudRate));
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Baud rate");
builder.setSingleChoiceItems(baudRates, pos, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
baudRate = Integer.parseInt(baudRates[item]);
dialog.dismiss();
}
});
builder.create().show();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
void refresh() {
UsbManager usbManager = (UsbManager) getActivity().getSystemService(Context.USB_SERVICE);
UsbSerialProber usbDefaultProber = UsbSerialProber.getDefaultProber();
UsbSerialProber usbCustomProber = CustomProber.getCustomProber();
listItems.clear();
for(UsbDevice device : usbManager.getDeviceList().values()) {
UsbSerialDriver driver = usbDefaultProber.probeDevice(device);
if(driver == null) {
driver = usbCustomProber.probeDevice(device);
}
if(driver != null) {
for(int port = 0; port < driver.getPorts().size(); port++)
listItems.add(new ListItem(device, port, driver));
} else {
listItems.add(new ListItem(device, 0, null));
}
}
listAdapter.notifyDataSetChanged();
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
ListItem item = listItems.get(position-1);
if(item.driver == null) {
Toast.makeText(getActivity(), "no driver", Toast.LENGTH_SHORT).show();
} else {
Bundle args = new Bundle();
args.putInt("device", item.device.getDeviceId());
args.putInt("port", item.port);
args.putInt("baud", baudRate);
Fragment fragment = new TerminalFragment();
fragment.setArguments(args);
getFragmentManager().beginTransaction().replace(R.id.fragment, fragment, "terminal").addToBackStack(null).commit();
}
}
}
package de.kai_morich.simple_usb_terminal;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
public class MainActivity extends AppCompatActivity implements FragmentManager.OnBackStackChangedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (savedInstanceState == null)
getSupportFragmentManager().beginTransaction().add(R.id.fragment, new DevicesFragment(), "devices").commit();
else
onBackStackChanged();
}
@Override
public void onBackStackChanged() {
getSupportActionBar().setDisplayHomeAsUpEnabled(getSupportFragmentManager().getBackStackEntryCount()>0);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
@Override
protected void onNewIntent(Intent intent) {
if ("android.hardware.usb.action.USB_DEVICE_ATTACHED".equals(intent.getAction())) {
TerminalFragment terminal = (TerminalFragment)getSupportFragmentManager().findFragmentByTag("terminal");
if (terminal != null)
terminal.status("USB device detected");
}
super.onNewIntent(intent);
}
}
package de.kai_morich.simple_usb_terminal;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.IBinder;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.TextUtils;
import android.widget.Toast;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.util.Log;
import java.lang.StringBuilder;
import com.hoho.android.usbserial.driver.SerialTimeoutException;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
public class MainActivity2 extends AppCompatActivity implements ServiceConnection, SerialListener {
private enum Connected { False, Pending, True }
private final BroadcastReceiver broadcastReceiver;
private int deviceId, portNum, baudRate;
private UsbSerialPort usbSerialPort;
private SerialService service;
private Connected connected = Connected.False;
private boolean initialStart = true;
private boolean isResume = true;
private String newline = TextUtil.newline_crlf;
private boolean pendingNewline = false;
private boolean pendingNewline2 = false;
private String receiveText = "";
public MainActivity2() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(Constants.INTENT_ACTION_GRANT_USB.equals(intent.getAction())) {
Boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false);
connect(granted);
}
}
};
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
deviceId = 1002;
portNum = 0;
baudRate = 115200;
this.bindService(new Intent(this, SerialService.class), this, Context.BIND_AUTO_CREATE);
status("onCreate");
isResume = false;
}
@Override
public void onDestroy() {
status("onDestroy");
try { this.unbindService(this); } catch(Exception ignored) {}
if (connected != Connected.False)
disconnect();
this.stopService(new Intent(this, SerialService.class));
super.onDestroy();
}
@Override
public void onStart() {
status("onStart");
super.onStart();
if(service != null)
service.attach(this);
else
this.startService(new Intent(this, SerialService.class)); // prevents service destroy on unbind from recreated activity caused by orientation change
}
@Override
public void onStop() {
status("onStop");
if(service != null && !this.isChangingConfigurations())
service.detach();
super.onStop();
}
// @SuppressWarnings("deprecation") // onAttach(context) was added with API 23. onAttach(activity) works for all API versions
// @Override
// public void onAttach(@NonNull Activity activity) {
// super.onAttach(activity);
// this.bindService(new Intent(this, SerialService.class), this, Context.BIND_AUTO_CREATE);
// }
//
// @Override
// public void onDetach() {
// try { getActivity().unbindService(this); } catch(Exception ignored) {}
// super.onDetach();
// }
@Override
public void onResume() {
super.onResume();
status("onResume");
this.registerReceiver(broadcastReceiver, new IntentFilter(Constants.INTENT_ACTION_GRANT_USB));
status("service: " + service);
initialStart = true;
if(initialStart && service != null) {
initialStart = false;
this.runOnUiThread(this::connect);
}
isResume = true;
// if(controlLinesEnabled && controlLines != null && connected == Connected.True)
// controlLines.start();
}
@Override
public void onPause() {
status("onPause");
this.unregisterReceiver(broadcastReceiver);
// if(controlLines != null)
// controlLines.stop();
super.onPause();
}
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = ((SerialService.SerialBinder) binder).getService();
service.attach(this);
if(initialStart && isResume) {
initialStart = false;
this.runOnUiThread(this::connect);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
}
@Override
protected void onNewIntent(Intent intent) {
if ("android.hardware.usb.action.USB_DEVICE_ATTACHED".equals(intent.getAction())) {
Toast.makeText(getApplicationContext(),"USB Device detectado", Toast.LENGTH_SHORT).show();
}
super.onNewIntent(intent);
}
/*
* Serial + UI
*/
private void connect() {
connect(null);
}
private void connect(Boolean permissionGranted) {
status("connect: " +permissionGranted);
UsbDevice device = null;
UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE);
for(UsbDevice v : usbManager.getDeviceList().values())
if(v.getDeviceId() == deviceId)
device = v;
if(device == null) {
status("connection failed: device not found");
return;
}
UsbSerialDriver driver = UsbSerialProber.getDefaultProber().probeDevice(device);
if(driver == null) {
driver = CustomProber.getCustomProber().probeDevice(device);
}
if(driver == null) {
status("connection failed: no driver for device");
return;
}
if(driver.getPorts().size() < portNum) {
status("connection failed: not enough ports at device");
return;
}
usbSerialPort = driver.getPorts().get(portNum);
UsbDeviceConnection usbConnection = usbManager.openDevice(driver.getDevice());
if(usbConnection == null && permissionGranted == null && !usbManager.hasPermission(driver.getDevice())) {
PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(Constants.INTENT_ACTION_GRANT_USB), 0);
usbManager.requestPermission(driver.getDevice(), usbPermissionIntent);
return;
}
if(usbConnection == null) {
if (!usbManager.hasPermission(driver.getDevice()))
status("connection failed: permission denied");
else
status("connection failed: open failed");
return;
}
connected = Connected.Pending;
try {
usbSerialPort.open(usbConnection);
usbSerialPort.setParameters(baudRate, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
SerialSocket socket = new SerialSocket(this.getApplicationContext(), usbConnection, usbSerialPort);
service.connect(socket);
// usb connect is not asynchronous. connect-success and connect-error are returned immediately from socket.connect
// for consistency to bluetooth/bluetooth-LE app use same SerialListener and SerialService classes
onSerialConnect();
} catch (Exception e) {
onSerialConnectError(e);
}
}
private void disconnect() {
connected = Connected.False;
// controlLines.stop();
service.disconnect();
usbSerialPort = null;
}
private void receive(byte[] data) {
String msg = new String(data);
if(newline.equals(TextUtil.newline_crlf) && msg.length() > 0) {
msg = msg.replace(TextUtil.newline_crlf, TextUtil.newline_lf);
pendingNewline = msg.charAt(msg.length() - 1) == '\r';
pendingNewline2 = msg.charAt(msg.length() - 1) == '\n';
receiveText = receiveText + msg;
receiveText = receiveText.replace("\n","");
if (pendingNewline || pendingNewline2) {
if (!TextUtils.isEmpty(receiveText)) {
Log.e("SERGIO", "FINAL: " + receiveText);
receiveText = "";
}
}
}
}
void status(String str) {
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
/*
* SerialListener
*/
@Override
public void onSerialConnect() {
status("connected");
connected = Connected.True;
// if(controlLinesEnabled)
// controlLines.start();
}
@Override
public void onSerialConnectError(Exception e) {
status("connection failed: " + e.getMessage());
disconnect();
}
@Override
public void onSerialRead(byte[] data) {
receive(data);
}
@Override
public void onSerialIoError(Exception e) {
status("connection lost: " + e.getMessage());
disconnect();
}
}
\ No newline at end of file
package de.kai_morich.simple_usb_terminal;
interface SerialListener {
void onSerialConnect ();
void onSerialConnectError (Exception e);
void onSerialRead (byte[] data);
void onSerialIoError (Exception e);
}
package de.kai_morich.simple_usb_terminal;
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.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
/**
* create notification and queue serial data while activity is not in the foreground
* use listener chain: SerialSocket -> SerialService -> UI fragment
*/
public class SerialService extends Service implements SerialListener {
class SerialBinder extends Binder {
SerialService getService() { return SerialService.this; }
}
private enum QueueType {Connect, ConnectError, Read, IoError}
private static class QueueItem {
QueueType type;
byte[] data;
Exception e;
QueueItem(QueueType type, byte[] data, Exception e) { this.type=type; this.data=data; this.e=e; }
}
private final Handler mainLooper;
private final IBinder binder;
private final Queue<QueueItem> queue1, queue2;
private SerialSocket socket;
private SerialListener listener;
private boolean connected;
/**
* Lifecylce
*/
public SerialService() {
mainLooper = new Handler(Looper.getMainLooper());
binder = new SerialBinder();
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
@Override
public void onDestroy() {
cancelNotification();
disconnect();
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
/**
* Api
*/
public void connect(SerialSocket socket) throws IOException {
socket.connect(this);
this.socket = socket;
connected = true;
}
public void disconnect() {
connected = false; // ignore data,errors while disconnecting
cancelNotification();
if(socket != null) {
socket.disconnect();
socket = null;
}
}
public void write(byte[] data) throws IOException {
if(!connected)
throw new IOException("not connected");
socket.write(data);
}
public void attach(SerialListener listener) {
if(Looper.getMainLooper().getThread() != Thread.currentThread())
throw new IllegalArgumentException("not in main thread");
cancelNotification();
// use synchronized() to prevent new items in queue2
// new items will not be added to queue1 because mainLooper.post and attach() run in main thread
synchronized (this) {
this.listener = listener;
}
for(QueueItem item : queue1) {
switch(item.type) {
case Connect: listener.onSerialConnect (); break;
case ConnectError: listener.onSerialConnectError (item.e); break;
case Read: listener.onSerialRead (item.data); break;
case IoError: listener.onSerialIoError (item.e); break;
}
}
for(QueueItem item : queue2) {
switch(item.type) {
case Connect: listener.onSerialConnect (); break;
case ConnectError: listener.onSerialConnectError (item.e); break;
case Read: listener.onSerialRead (item.data); break;
case IoError: listener.onSerialIoError (item.e); break;
}
}
queue1.clear();
queue2.clear();
}
public void detach() {
if(connected)
createNotification();
// items already in event queue (posted before detach() to mainLooper) will end up in queue1
// items occurring later, will be moved directly to queue2
// detach() and mainLooper.post run in the main thread, so all items are caught
listener = null;
}
private void createNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel nc = new NotificationChannel(Constants.NOTIFICATION_CHANNEL, "Background service", NotificationManager.IMPORTANCE_LOW);
nc.setShowBadge(false);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.createNotificationChannel(nc);
}
Intent disconnectIntent = new Intent()
.setAction(Constants.INTENT_ACTION_DISCONNECT);
Intent restartIntent = new Intent()
.setClassName(this, Constants.INTENT_CLASS_MAIN_ACTIVITY)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER);
PendingIntent disconnectPendingIntent = PendingIntent.getBroadcast(this, 1, disconnectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent restartPendingIntent = PendingIntent.getActivity(this, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL)
.setSmallIcon(R.drawable.ic_notification)
.setColor(getResources().getColor(R.color.colorPrimary))
.setContentTitle(getResources().getString(R.string.app_name))
.setContentText(socket != null ? "Connected to "+socket.getName() : "Background Service")
.setContentIntent(restartPendingIntent)
.setOngoing(true)
.addAction(new NotificationCompat.Action(R.drawable.ic_clear_white_24dp, "Disconnect", disconnectPendingIntent));
// @drawable/ic_notification created with Android Studio -> New -> Image Asset using @color/colorPrimaryDark as background color
// Android < API 21 does not support vectorDrawables in notifications, so both drawables used here, are created as .png instead of .xml
Notification notification = builder.build();
startForeground(Constants.NOTIFY_MANAGER_START_FOREGROUND_SERVICE, notification);
}
private void cancelNotification() {
stopForeground(true);
}
/**
* SerialListener
*/
public void onSerialConnect() {
if(connected) {
synchronized (this) {
if (listener != null) {
mainLooper.post(() -> {
if (listener != null) {
listener.onSerialConnect();
} else {
queue1.add(new QueueItem(QueueType.Connect, null, null));
}
});
} else {
queue2.add(new QueueItem(QueueType.Connect, null, null));
}
}
}
}
public void onSerialConnectError(Exception e) {
if(connected) {
synchronized (this) {
if (listener != null) {
mainLooper.post(() -> {
if (listener != null) {
listener.onSerialConnectError(e);
} else {
queue1.add(new QueueItem(QueueType.ConnectError, null, e));
cancelNotification();
disconnect();
}
});
} else {
queue2.add(new QueueItem(QueueType.ConnectError, null, e));
cancelNotification();
disconnect();
}
}
}
}
public void onSerialRead(byte[] data) {
if(connected) {
synchronized (this) {
if (listener != null) {
mainLooper.post(() -> {
if (listener != null) {
listener.onSerialRead(data);
} else {
queue1.add(new QueueItem(QueueType.Read, data, null));
}
});
} else {
queue2.add(new QueueItem(QueueType.Read, data, null));
}
}
}
}
public void onSerialIoError(Exception e) {
if(connected) {
synchronized (this) {
if (listener != null) {
mainLooper.post(() -> {
if (listener != null) {
listener.onSerialIoError(e);
} else {
queue1.add(new QueueItem(QueueType.IoError, null, e));
cancelNotification();
disconnect();
}
});
} else {
queue2.add(new QueueItem(QueueType.IoError, null, e));
cancelNotification();
disconnect();
}
}
}
}
}
package de.kai_morich.simple_usb_terminal;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDeviceConnection;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.util.SerialInputOutputManager;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.concurrent.Executors;
public class SerialSocket implements SerialInputOutputManager.Listener {
private static final int WRITE_WAIT_MILLIS = 2000; // 0 blocked infinitely on unprogrammed arduino
private final BroadcastReceiver disconnectBroadcastReceiver;
private final Context context;
private SerialListener listener;
private UsbDeviceConnection connection;
private UsbSerialPort serialPort;
private SerialInputOutputManager ioManager;
SerialSocket(Context context, UsbDeviceConnection connection, UsbSerialPort serialPort) {
if(context instanceof Activity)
throw new InvalidParameterException("expected non UI context");
this.context = context;
this.connection = connection;
this.serialPort = serialPort;
disconnectBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (listener != null)
listener.onSerialIoError(new IOException("background disconnect"));
disconnect(); // disconnect now, else would be queued until UI re-attached
}
};
}
String getName() { return serialPort.getDriver().getClass().getSimpleName().replace("SerialDriver",""); }
void connect(SerialListener listener) throws IOException {
this.listener = listener;
context.registerReceiver(disconnectBroadcastReceiver, new IntentFilter(Constants.INTENT_ACTION_DISCONNECT));
serialPort.setDTR(true); // for arduino, ...
serialPort.setRTS(true);
ioManager = new SerialInputOutputManager(serialPort, this);
ioManager.start();
}
void disconnect() {
listener = null; // ignore remaining data and errors
if (ioManager != null) {
ioManager.setListener(null);
ioManager.stop();
ioManager = null;
}
if (serialPort != null) {
try {
serialPort.setDTR(false);
serialPort.setRTS(false);
} catch (Exception ignored) {
}
try {
serialPort.close();
} catch (Exception ignored) {
}
serialPort = null;
}
if(connection != null) {
connection.close();
connection = null;
}
try {
context.unregisterReceiver(disconnectBroadcastReceiver);
} catch (Exception ignored) {
}
}
void write(byte[] data) throws IOException {
if(serialPort == null)
throw new IOException("not connected");
serialPort.write(data, WRITE_WAIT_MILLIS);
}
@Override
public void onNewData(byte[] data) {
if(listener != null)
listener.onSerialRead(data);
}
@Override
public void onRunError(Exception e) {
if (listener != null)
listener.onSerialIoError(e);
}
}
package de.kai_morich.simple_usb_terminal;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.hoho.android.usbserial.driver.SerialTimeoutException;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import java.io.IOException;
import java.util.EnumSet;
import android.util.Log;
public class TerminalFragment extends Fragment implements ServiceConnection, SerialListener {
private enum Connected { False, Pending, True }
private final BroadcastReceiver broadcastReceiver;
private int deviceId, portNum, baudRate;
private UsbSerialPort usbSerialPort;
private SerialService service;
private TextView receiveText;
private TextView sendText;
private ControlLines controlLines;
private TextUtil.HexWatcher hexWatcher;
private Connected connected = Connected.False;
private boolean initialStart = true;
private boolean hexEnabled = false;
private boolean controlLinesEnabled = false;
private boolean pendingNewline = false;
private String newline = TextUtil.newline_crlf;
public TerminalFragment() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(Constants.INTENT_ACTION_GRANT_USB.equals(intent.getAction())) {
Boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false);
connect(granted);
}
}
};
}
/*
* Lifecycle
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
setRetainInstance(true);
deviceId = getArguments().getInt("device");
portNum = getArguments().getInt("port");
baudRate = getArguments().getInt("baud");
}
@Override
public void onDestroy() {
if (connected != Connected.False)
disconnect();
getActivity().stopService(new Intent(getActivity(), SerialService.class));
super.onDestroy();
}
@Override
public void onStart() {
super.onStart();
if(service != null)
service.attach(this);
else
getActivity().startService(new Intent(getActivity(), SerialService.class)); // prevents service destroy on unbind from recreated activity caused by orientation change
}
@Override
public void onStop() {
if(service != null && !getActivity().isChangingConfigurations())
service.detach();
super.onStop();
}
@SuppressWarnings("deprecation") // onAttach(context) was added with API 23. onAttach(activity) works for all API versions
@Override
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
getActivity().bindService(new Intent(getActivity(), SerialService.class), this, Context.BIND_AUTO_CREATE);
}
@Override
public void onDetach() {
try { getActivity().unbindService(this); } catch(Exception ignored) {}
super.onDetach();
}
@Override
public void onResume() {
super.onResume();
getActivity().registerReceiver(broadcastReceiver, new IntentFilter(Constants.INTENT_ACTION_GRANT_USB));
if(initialStart && service != null) {
initialStart = false;
getActivity().runOnUiThread(this::connect);
}
if(controlLinesEnabled && controlLines != null && connected == Connected.True)
controlLines.start();
}
@Override
public void onPause() {
getActivity().unregisterReceiver(broadcastReceiver);
if(controlLines != null)
controlLines.stop();
super.onPause();
}
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = ((SerialService.SerialBinder) binder).getService();
service.attach(this);
if(initialStart && isResumed()) {
initialStart = false;
getActivity().runOnUiThread(this::connect);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
}
/*
* UI
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_terminal, container, false);
receiveText = view.findViewById(R.id.receive_text); // TextView performance decreases with number of spans
receiveText.setTextColor(getResources().getColor(R.color.colorRecieveText)); // set as default color to reduce number of spans
receiveText.setMovementMethod(ScrollingMovementMethod.getInstance());
sendText = view.findViewById(R.id.send_text);
hexWatcher = new TextUtil.HexWatcher(sendText);
hexWatcher.enable(hexEnabled);
sendText.addTextChangedListener(hexWatcher);
sendText.setHint(hexEnabled ? "HEX mode" : "");
View sendBtn = view.findViewById(R.id.send_btn);
sendBtn.setOnClickListener(v -> send(sendText.getText().toString()));
controlLines = new ControlLines(view);
return view;
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_terminal, menu);
menu.findItem(R.id.hex).setChecked(hexEnabled);
menu.findItem(R.id.controlLines).setChecked(controlLinesEnabled);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.clear) {
receiveText.setText("");
return true;
} else if (id == R.id.newline) {
String[] newlineNames = getResources().getStringArray(R.array.newline_names);
String[] newlineValues = getResources().getStringArray(R.array.newline_values);
int pos = java.util.Arrays.asList(newlineValues).indexOf(newline);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Newline");
builder.setSingleChoiceItems(newlineNames, pos, (dialog, item1) -> {
newline = newlineValues[item1];
dialog.dismiss();
});
builder.create().show();
return true;
} else if (id == R.id.hex) {
hexEnabled = !hexEnabled;
sendText.setText("");
hexWatcher.enable(hexEnabled);
sendText.setHint(hexEnabled ? "HEX mode" : "");
item.setChecked(hexEnabled);
return true;
} else if (id == R.id.controlLines) {
controlLinesEnabled = !controlLinesEnabled;
item.setChecked(controlLinesEnabled);
if (controlLinesEnabled) {
controlLines.start();
} else {
controlLines.stop();
}
return true;
} else if (id == R.id.sendBreak) {
try {
usbSerialPort.setBreak(true);
Thread.sleep(100);
status("send BREAK");
usbSerialPort.setBreak(false);
} catch (Exception e) {
status("send BREAK failed: " + e.getMessage());
}
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
/*
* Serial + UI
*/
private void connect() {
connect(null);
}
private void connect(Boolean permissionGranted) {
UsbDevice device = null;
UsbManager usbManager = (UsbManager) getActivity().getSystemService(Context.USB_SERVICE);
for(UsbDevice v : usbManager.getDeviceList().values())
if(v.getDeviceId() == deviceId)
device = v;
if(device == null) {
status("connection failed: device not found");
return;
}
UsbSerialDriver driver = UsbSerialProber.getDefaultProber().probeDevice(device);
if(driver == null) {
driver = CustomProber.getCustomProber().probeDevice(device);
}
if(driver == null) {
status("connection failed: no driver for device");
return;
}
if(driver.getPorts().size() < portNum) {
status("connection failed: not enough ports at device");
return;
}
usbSerialPort = driver.getPorts().get(portNum);
UsbDeviceConnection usbConnection = usbManager.openDevice(driver.getDevice());
if(usbConnection == null && permissionGranted == null && !usbManager.hasPermission(driver.getDevice())) {
PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(getActivity(), 0, new Intent(Constants.INTENT_ACTION_GRANT_USB), 0);
usbManager.requestPermission(driver.getDevice(), usbPermissionIntent);
return;
}
if(usbConnection == null) {
if (!usbManager.hasPermission(driver.getDevice()))
status("connection failed: permission denied");
else
status("connection failed: open failed");
return;
}
connected = Connected.Pending;
try {
usbSerialPort.open(usbConnection);
usbSerialPort.setParameters(baudRate, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
SerialSocket socket = new SerialSocket(getActivity().getApplicationContext(), usbConnection, usbSerialPort);
service.connect(socket);
// usb connect is not asynchronous. connect-success and connect-error are returned immediately from socket.connect
// for consistency to bluetooth/bluetooth-LE app use same SerialListener and SerialService classes
onSerialConnect();
} catch (Exception e) {
onSerialConnectError(e);
}
}
private void disconnect() {
connected = Connected.False;
controlLines.stop();
service.disconnect();
usbSerialPort = null;
}
private void send(String str) {
if(connected != Connected.True) {
Toast.makeText(getActivity(), "not connected", Toast.LENGTH_SHORT).show();
return;
}
try {
String msg;
byte[] data;
if(hexEnabled) {
StringBuilder sb = new StringBuilder();
TextUtil.toHexString(sb, TextUtil.fromHexString(str));
TextUtil.toHexString(sb, newline.getBytes());
msg = sb.toString();
data = TextUtil.fromHexString(msg);
} else {
msg = str;
data = (str + newline).getBytes();
}
SpannableStringBuilder spn = new SpannableStringBuilder(msg + '\n');
spn.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorSendText)), 0, spn.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
receiveText.append(spn);
service.write(data);
} catch (SerialTimeoutException e) {
status("write timeout: " + e.getMessage());
} catch (Exception e) {
onSerialIoError(e);
}
}
private void receive(byte[] data) {
if(hexEnabled) {
receiveText.append(TextUtil.toHexString(data) + '\n');
} else {
String msg = new String(data);
Log.e("SERGIO", "1: " + msg);
if(newline.equals(TextUtil.newline_crlf) && msg.length() > 0) {
// don't show CR as ^M if directly before LF
msg = msg.replace(TextUtil.newline_crlf, TextUtil.newline_lf);
Log.e("SERGIO", "2: " +msg);
// special handling if CR and LF come in separate fragments
if (pendingNewline && msg.charAt(0) == '\n') {
Editable edt = receiveText.getEditableText();
if (edt != null && edt.length() > 1) {
edt.replace(edt.length() - 2, edt.length(), "");
Log.e("SERGIO", "3: " + receiveText.getText().toString());
}
}
pendingNewline = msg.charAt(msg.length() - 1) == '\r';
Log.e("SERGIO", "4: " + msg);
}
Log.e("SERGIO", "5: " + TextUtil.toCaretString(msg, newline.length() != 0));
receiveText.append(TextUtil.toCaretString(msg, newline.length() != 0));
Log.e("SERGIO", "6: " + receiveText.getText().toString());
}
}
void status(String str) {
SpannableStringBuilder spn = new SpannableStringBuilder( str + '\n');
spn.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorStatusText)), 0, spn.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
receiveText.append(spn);
}
/*
* SerialListener
*/
@Override
public void onSerialConnect() {
status("connected");
connected = Connected.True;
if(controlLinesEnabled)
controlLines.start();
}
@Override
public void onSerialConnectError(Exception e) {
status("connection failed: " + e.getMessage());
disconnect();
}
@Override
public void onSerialRead(byte[] data) {
receive(data);
}
@Override
public void onSerialIoError(Exception e) {
status("connection lost: " + e.getMessage());
disconnect();
}
class ControlLines {
private static final int refreshInterval = 200; // msec
private final Handler mainLooper;
private final Runnable runnable;
private final LinearLayout frame;
private final ToggleButton rtsBtn, ctsBtn, dtrBtn, dsrBtn, cdBtn, riBtn;
ControlLines(View view) {
mainLooper = new Handler(Looper.getMainLooper());
runnable = this::run; // w/o explicit Runnable, a new lambda would be created on each postDelayed, which would not be found again by removeCallbacks
frame = view.findViewById(R.id.controlLines);
rtsBtn = view.findViewById(R.id.controlLineRts);
ctsBtn = view.findViewById(R.id.controlLineCts);
dtrBtn = view.findViewById(R.id.controlLineDtr);
dsrBtn = view.findViewById(R.id.controlLineDsr);
cdBtn = view.findViewById(R.id.controlLineCd);
riBtn = view.findViewById(R.id.controlLineRi);
rtsBtn.setOnClickListener(this::toggle);
dtrBtn.setOnClickListener(this::toggle);
}
private void toggle(View v) {
ToggleButton btn = (ToggleButton) v;
if (connected != Connected.True) {
btn.setChecked(!btn.isChecked());
Toast.makeText(getActivity(), "not connected", Toast.LENGTH_SHORT).show();
return;
}
String ctrl = "";
try {
if (btn.equals(rtsBtn)) { ctrl = "RTS"; usbSerialPort.setRTS(btn.isChecked()); }
if (btn.equals(dtrBtn)) { ctrl = "DTR"; usbSerialPort.setDTR(btn.isChecked()); }
} catch (IOException e) {
status("set" + ctrl + " failed: " + e.getMessage());
}
}
private void run() {
if (connected != Connected.True)
return;
try {
EnumSet<UsbSerialPort.ControlLine> controlLines = usbSerialPort.getControlLines();
rtsBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.RTS));
ctsBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.CTS));
dtrBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.DTR));
dsrBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.DSR));
cdBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.CD));
riBtn.setChecked(controlLines.contains(UsbSerialPort.ControlLine.RI));
mainLooper.postDelayed(runnable, refreshInterval);
} catch (IOException e) {
status("getControlLines() failed: " + e.getMessage() + " -> stopped control line refresh");
}
}
void start() {
frame.setVisibility(View.VISIBLE);
if (connected != Connected.True)
return;
try {
EnumSet<UsbSerialPort.ControlLine> controlLines = usbSerialPort.getSupportedControlLines();
if (!controlLines.contains(UsbSerialPort.ControlLine.RTS)) rtsBtn.setVisibility(View.INVISIBLE);
if (!controlLines.contains(UsbSerialPort.ControlLine.CTS)) ctsBtn.setVisibility(View.INVISIBLE);
if (!controlLines.contains(UsbSerialPort.ControlLine.DTR)) dtrBtn.setVisibility(View.INVISIBLE);
if (!controlLines.contains(UsbSerialPort.ControlLine.DSR)) dsrBtn.setVisibility(View.INVISIBLE);
if (!controlLines.contains(UsbSerialPort.ControlLine.CD)) cdBtn.setVisibility(View.INVISIBLE);
if (!controlLines.contains(UsbSerialPort.ControlLine.RI)) riBtn.setVisibility(View.INVISIBLE);
run();
} catch (IOException e) {
Toast.makeText(getActivity(), "getSupportedControlLines() failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
void stop() {
frame.setVisibility(View.GONE);
mainLooper.removeCallbacks(runnable);
rtsBtn.setChecked(false);
ctsBtn.setChecked(false);
dtrBtn.setChecked(false);
dsrBtn.setChecked(false);
cdBtn.setChecked(false);
riBtn.setChecked(false);
}
}
}
\ No newline at end of file
package de.kai_morich.simple_usb_terminal;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import java.io.ByteArrayOutputStream;
final class TextUtil {
@ColorInt static int caretBackground = 0xff666666;
final static String newline_crlf = "\r\n";
final static String newline_lf = "\n";
static byte[] fromHexString(final CharSequence s) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte b = 0;
int nibble = 0;
for(int pos = 0; pos<s.length(); pos++) {
if(nibble==2) {
buf.write(b);
nibble = 0;
b = 0;
}
int c = s.charAt(pos);
if(c>='0' && c<='9') { nibble++; b *= 16; b += c-'0'; }
if(c>='A' && c<='F') { nibble++; b *= 16; b += c-'A'+10; }
if(c>='a' && c<='f') { nibble++; b *= 16; b += c-'a'+10; }
}
if(nibble>0)
buf.write(b);
return buf.toByteArray();
}
static String toHexString(final byte[] buf) {
return toHexString(buf, 0, buf.length);
}
static String toHexString(final byte[] buf, int begin, int end) {
StringBuilder sb = new StringBuilder(3*(end-begin));
toHexString(sb, buf, begin, end);
return sb.toString();
}
static void toHexString(StringBuilder sb, final byte[] buf) {
toHexString(sb, buf, 0, buf.length);
}
static void toHexString(StringBuilder sb, final byte[] buf, int begin, int end) {
for(int pos=begin; pos<end; pos++) {
if(sb.length()>0)
sb.append(' ');
int c;
c = (buf[pos]&0xff) / 16;
if(c >= 10) c += 'A'-10;
else c += '0';
sb.append((char)c);
c = (buf[pos]&0xff) % 16;
if(c >= 10) c += 'A'-10;
else c += '0';
sb.append((char)c);
}
}
/**
* use https://en.wikipedia.org/wiki/Caret_notation to avoid invisible control characters
*/
static CharSequence toCaretString(CharSequence s, boolean keepNewline) {
return toCaretString(s, keepNewline, s.length());
}
static CharSequence toCaretString(CharSequence s, boolean keepNewline, int length) {
boolean found = false;
for (int pos = 0; pos < length; pos++) {
if (s.charAt(pos) < 32 && (!keepNewline ||s.charAt(pos)!='\n')) {
found = true;
break;
}
}
if(!found)
return s;
SpannableStringBuilder sb = new SpannableStringBuilder();
for(int pos=0; pos<length; pos++)
if (s.charAt(pos) < 32 && (!keepNewline ||s.charAt(pos)!='\n')) {
sb.append('^');
sb.append((char)(s.charAt(pos) + 64));
sb.setSpan(new BackgroundColorSpan(caretBackground), sb.length()-2, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
sb.append(s.charAt(pos));
}
return sb;
}
static class HexWatcher implements TextWatcher {
private final TextView view;
private final StringBuilder sb = new StringBuilder();
private boolean self = false;
private boolean enabled = false;
HexWatcher(TextView view) {
this.view = view;
}
void enable(boolean enable) {
if(enable) {
view.setInputType(InputType.TYPE_CLASS_TEXT + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
} else {
view.setInputType(InputType.TYPE_CLASS_TEXT + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
enabled = enable;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if(!enabled || self)
return;
sb.delete(0,sb.length());
int i;
for(i=0; i<s.length(); i++) {
char c = s.charAt(i);
if(c >= '0' && c <= '9') sb.append(c);
if(c >= 'A' && c <= 'F') sb.append(c);
if(c >= 'a' && c <= 'f') sb.append((char)(c+'A'-'a'));
}
for(i=2; i<sb.length(); i+=3)
sb.insert(i,' ');
final String s2 = sb.toString();
if(!s2.equals(s.toString())) {
self = true;
s.replace(0, s.length(), s2);
self = false;
}
}
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity2">
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/listDivider"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="@string/devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/text2"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/controlLines"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<ToggleButton
android:id="@+id/controlLineRts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48sp"
android:textOff="RTS"
android:textOn="RTS" />
<ToggleButton
android:id="@+id/controlLineCts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48sp"
android:clickable="false"
android:textColor="@android:color/secondary_text_dark"
android:textOff="CTS"
android:textOn="CTS" />
<View
android:layout_height="match_parent"
android:layout_width="6dp" />
<ToggleButton
android:id="@+id/controlLineDtr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48sp"
android:textOff="DTR"
android:textOn="DTR" />
<ToggleButton
android:id="@+id/controlLineDsr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:minWidth="48sp"
android:textColor="@android:color/secondary_text_dark"
android:textOff="DSR"
android:textOn="DSR" />
<View
android:layout_height="match_parent"
android:layout_width="6dp" />
<ToggleButton
android:id="@+id/controlLineCd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:minWidth="48sp"
android:textColor="@android:color/secondary_text_dark"
android:textOff="CD"
android:textOn="CD" />
<ToggleButton
android:id="@+id/controlLineRi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48sp"
android:clickable="false"
android:textColor="@android:color/secondary_text_dark"
android:textOff="RI"
android:textOn="RI" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:background="?android:attr/listDivider"
android:layout_height="2dp" />
<TextView
android:id="@+id/receive_text"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:freezesText="true"
android:gravity="bottom"
android:scrollbars="vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<View
android:layout_width="match_parent"
android:background="?android:attr/listDivider"
android:layout_height="2dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/send_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:inputType="text|textNoSuggestions"
android:singleLine="true" />
<ImageButton
android:id="@+id/send_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_send_white_24dp" />
</LinearLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/refresh"
android:title="Refresh Devices" />
<item
android:id="@+id/baud_rate"
android:title="Baud rate" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/clear"
android:icon="@drawable/ic_delete_white_24dp"
android:title="Clear"
app:showAsAction="always" />
<item
android:id="@+id/newline"
android:title="Newline"
app:showAsAction="never" />
<item
android:id="@+id/hex"
android:title="HEX Mode"
android:checkable="true"
app:showAsAction="never" />
<item
android:id="@+id/controlLines"
android:title="Control Lines"
android:checkable="true"
app:showAsAction="never" />
<item
android:id="@+id/sendBreak"
android:title="Send BREAK"
app:showAsAction="never" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="baud_rates">
<item>2400</item>
<item>9600</item>
<item>19200</item>
<item>57600</item>
<item>115200</item>
</string-array>
<string-array name="newline_names">
<item>CR+LF</item>
<item>LF</item>
<item>&lt;none&gt;</item>
</string-array>
<string-array name="newline_values">
<item>\u000d\u000a</item>
<item>\u000a</item>
<item></item>
</string-array>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="colorPrimary">#d84315</color>
<color name="colorPrimaryDark">#bf360c</color>
<color name="colorAccent">#ff6e40</color>
<color name="colorRecieveText">#00FF00</color>
<color name="colorSendText">#82CAFF</color>
<color name="colorStatusText">#FFDB58</color>
</resources>
<resources>
<string name="app_name">ConnectUSBSerial</string>
<string name="app_name">Simple USB Terminal</string>
<string name="devices">USB Devices</string>
</resources>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- - - - default prober - - - -->
<!-- 0x0403 / 0x60??: FTDI -->
<usb-device vendor-id="1027" product-id="24577" /> <!-- 0x6001: FT232R -->
<usb-device vendor-id="1027" product-id="24592" /> <!-- 0x6010: FT2232H -->
<usb-device vendor-id="1027" product-id="24593" /> <!-- 0x6011: FT4232H -->
<usb-device vendor-id="1027" product-id="24596" /> <!-- 0x6014: FT232H -->
<usb-device vendor-id="1027" product-id="24597" /> <!-- 0x6015: FT230X, FT231X, FT234XD -->
<!-- 0x10C4 / 0xEA??: Silabs CP210x -->
<usb-device vendor-id="4292" product-id="60000" /> <!-- 0xea60: CP2102 and other CP210x single port devices -->
<usb-device vendor-id="4292" product-id="60016" /> <!-- 0xea70: CP2105 -->
<usb-device vendor-id="4292" product-id="60017" /> <!-- 0xea71: CP2108 -->
<!-- 0x067B / 0x23?3: Prolific PL2303x -->
<usb-device vendor-id="1659" product-id="8963" /> <!-- 0x2303: PL2303HX, HXD, TA, ... -->
<usb-device vendor-id="1659" product-id="9123" /> <!-- 0x23a3: PL2303GC -->
<usb-device vendor-id="1659" product-id="9139" /> <!-- 0x23b3: PL2303GB -->
<usb-device vendor-id="1659" product-id="9155" /> <!-- 0x23c3: PL2303GT -->
<usb-device vendor-id="1659" product-id="9171" /> <!-- 0x23d3: PL2303GL -->
<usb-device vendor-id="1659" product-id="9187" /> <!-- 0x23e3: PL2303GE -->
<usb-device vendor-id="1659" product-id="9203" /> <!-- 0x23f3: PL2303GS -->
<!-- 0x1a86 / 0x?523: Qinheng CH34x -->
<usb-device vendor-id="6790" product-id="21795" /> <!-- 0x5523: CH341A -->
<usb-device vendor-id="6790" product-id="29987" /> <!-- 0x7523: CH340 -->
<!-- CDC driver -->
<usb-device vendor-id="9025" /> <!-- 0x2341 / ......: Arduino -->
<usb-device vendor-id="5824" product-id="1155" /> <!-- 0x16C0 / 0x0483: Teensyduino -->
<usb-device vendor-id="1003" product-id="8260" /> <!-- 0x03EB / 0x2044: Atmel Lufa -->
<usb-device vendor-id="7855" product-id="4" /> <!-- 0x1eaf / 0x0004: Leaflabs Maple -->
<usb-device vendor-id="3368" product-id="516" /> <!-- 0x0d28 / 0x0204: ARM mbed -->
<usb-device vendor-id="1155" product-id="22336" /><!-- 0x0483 / 0x5740: ST CDC -->
<!-- - - - custom prober - - - -->
<!-- CDC driver -->
<usb-device vendor-id="5840" product-id="2174" /> <!-- 0x16d0 / 0x087e: Digispark -->
</resources>
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath 'com.android.tools.build:gradle:7.0.2'
}
}
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
allprojects {
repositories {
google()
mavenCentral()
mavenLocal()
maven { url 'https://jitpack.io' }
}
}
......
......@@ -6,14 +6,10 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
\ No newline at end of file
#Mon Nov 15 13:26:21 CET 2021
#Thu Oct 15 17:47:46 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
......@@ -44,7 +28,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
......@@ -82,7 +66,6 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
......@@ -126,11 +109,10 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
......@@ -156,19 +138,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
i=$((i+1))
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
......@@ -177,9 +159,14 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
......@@ -29,18 +13,15 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
......@@ -54,7 +35,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
......@@ -64,14 +45,28 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
......
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
rootProject.name = "ConnectUSBSerial"
include ':app'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment