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 androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import android.os.Looper; 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.TextView; import android.widget.Toast; import android.widget.ToggleButton; 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; public class TerminalFragment extends Fragment implements ServiceConnection, SerialListener { private enum Connected { False, Pending, True } private int deviceId, portNum, baudRate; private String newline = "\r\n"; private TextView receiveText; private UsbSerialPort usbSerialPort; private SerialService service; private boolean initialStart = true; private Connected connected = Connected.False; private BroadcastReceiver broadcastReceiver; private ControlLines controlLines; public TerminalFragment() { broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equals(Constants.INTENT_ACTION_GRANT_USB)) { 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(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()); TextView sendText = view.findViewById(R.id.send_text); 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); } @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 { 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 { SpannableStringBuilder spn = new SpannableStringBuilder(str+'\n'); spn.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorSendText)), 0, spn.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); receiveText.append(spn); byte[] data = (str + newline).getBytes(); service.write(data); } catch (Exception e) { onSerialIoError(e); } } private void receive(byte[] data) { receiveText.append(new String(data)); } 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; 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 Handler mainLooper; private Runnable runnable; private ToggleButton rtsBtn, ctsBtn, dtrBtn, dsrBtn, cdBtn, riBtn; ControlLines(View view) { mainLooper = new Handler(Looper.getMainLooper()); runnable = this::start; // w/o explicit Runnable, a new lambda would be created on each postDelayed, which would not be found again by removeCallbacks 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 boolean refresh() { String ctrl = ""; try { ctrl = "RTS"; rtsBtn.setChecked(usbSerialPort.getRTS()); ctrl = "CTS"; ctsBtn.setChecked(usbSerialPort.getCTS()); ctrl = "DTR"; dtrBtn.setChecked(usbSerialPort.getDTR()); ctrl = "DSR"; dsrBtn.setChecked(usbSerialPort.getDSR()); ctrl = "CD"; cdBtn.setChecked(usbSerialPort.getCD()); ctrl = "RI"; riBtn.setChecked(usbSerialPort.getRI()); } catch (IOException e) { status("get" + ctrl + " failed: " + e.getMessage() + " -> stopped control line refresh"); return false; } return true; } void start() { if (connected == Connected.True && refresh()) mainLooper.postDelayed(runnable, refreshInterval); } void stop() { mainLooper.removeCallbacks(runnable); } } }