ipPrint4
This is an Android label/receipt printing app for TCP/IP connected printers.
This app is based on my btPrint4 app. In contrast to btPrint4, this time we print on TCP/IP connected printers.
As with btPrint4, we have a main activity and one to list available printers and one to list available demo files.
The challenge with ipPrint4 was a replacement for the bluetooth device discovery. This time, we have to scan TCP/IP address range for port 9100. This port is also called HP direct printing port and supported by many printers. It behaves similar to telnet and you can just send print commands to the interface.
The second main change to btPrint4 was the printing code. This time, we do not have to use a bluetooth socket but a network TCP/IP socket.
A TCP/IP Portscanner
If you scan a range of IP addresses in sequence and try to open a port with a timeout of, let’s say 200ms, the scan will take (200msx254, scan from 1 to 254) 50 seconds.
Port Scan Code
...
ScanResult portIsOpen1(String sIp, int p, int timeout){
ScanResult scanResult=new ScanResult(sIp, p, false);
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(sIp, p), timeout);
socket.close();
scanResult = new ScanResult(sIp, p, true);
} catch (Exception ex) {
doLog("Exception in portIsOpen1 for " + sIp + "/" + p);
}
return scanResult;
}
...
for (int ip1=1; ip1<=254; ip1++){
String sip=String.format(baseIP + ".%03d", ip1);
scanResult = portIsOpen1(sip, port, timeout);
if(scanResult.isOpen){
Log.i(TAG, scanResult.sIP + " 9100 open");
msg = mHandler.obtainMessage(msgTypes.addHost);
bundle = new Bundle();
bundle.putString(msgTypes.HOST_NAME, scanResult.sIP);
msg.setData(bundle);
mHandler.sendMessage(msg);
doLog("added host msg for " + scanResult.sIP);
}
else{
Log.i(TAG, scanResult.sIP + " 9100 unavailable");
}
if(backgroundThread.interrupted())
break;
}
...
That long time seems too much for me to let the user wait. Fortunately, there is a post at stackoverflow showing how to use an executer service to run multiple port scans in parallel.
Start Multiple Port Scans At Once
...
static Future<ScanResult> portIsOpen
(final ExecutorService es, final String ip, final int port, final int timeout) {
return es.submit(new Callable<ScanResult>() {
@Override public ScanResult call() {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(ip, port), timeout);
socket.close();
return new ScanResult(ip, port, true);
} catch (Exception ex) {
return new ScanResult(ip, port, false);
}
}
});
}
...
synchronized void startDiscovery1(){
...
doLog("startDiscovery1: starting futures...");
for (int ip1=1; ip1<=254; ip1++){
String sip=String.format(baseIP + ".%03d", ip1);
futures.add(portIsOpen(es, sip, port, timeout));
}
doLog("startDiscovery1: es.shutdown()");
es.shutdown();
int openPorts = 0;
doLog("startDiscovery1 getting results...");
for (final Future<ScanResult> f : futures) {
try {
if (f.get().isOpen) {
openPorts++;
Log.i("portScan:", f.get().sIP + " 9100 open");
msg = mHandler.obtainMessage(msgTypes.addHost);
bundle = new Bundle();
bundle.putString(msgTypes.HOST_NAME, f.get().sIP);
msg.setData(bundle);
mHandler.sendMessage(msg);
doLog("added host msg for " + f.get().sIP);
}
else{
Log.i("portScan:", f.get().sIP + " 9100 closed");
}
}
catch(ExecutionException e){
doLog("ExecutionException: "+e.getMessage());
}
catch(InterruptedException e){
doLog("InterruptedException: "+e.getMessage());
}
}
...
}
...
Unfortunately, the start of the Future objects is blocking and so I had to wrap that in another thread, so the caller (the GUI) is not blocked.
Thread to Start Port Scans
...
@Override
public void run() {
doLog("thread: run()...");
if(state!=eState.idle) {
doLog("thread: run() ended as state!=idle");
return;
}
state=eState.running;
ScanResult scanResult;
try {
doLog("thread: starting...");
msg=mHandler.obtainMessage(msgTypes.started);
mHandler.sendMessage(msg);
if( true) {
doLog("thread: starting discovery...");
startDiscovery1();
...
...
...
With the above solution, the port scan finishes with 5 to 10 seconds. Every time a new open port is found, the IP is added to the list. The process shows a progress cycle in the title of the list view activity. All status changes are sent via a message handler from the portscanner code to the calling list activity.
When a TCP/IP address with port 9100 is found and selected, you can print a demo file to it. You can also enter an IP manually and then directly print a demo file to it.
Startup Checks
On startup, the app checks for TCP/IP connectivity and ends, if there is no newtwork connection. It makes no sense to run the app without a working connection.
Check Online Status
...
public static boolean isNetworkOnline(Context context) {
boolean status=false;
try{
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getNetworkInfo(0);
if (netInfo != null && netInfo.getState()==NetworkInfo.State.CONNECTED) {
status= true;
}else {
netInfo = cm.getNetworkInfo(1);
if(netInfo!=null && netInfo.getState()==NetworkInfo.State.CONNECTED)
status= true;
}
}catch(Exception e){
e.printStackTrace();
return false;
}
return status;
}
...
The local IP is automatically inserted as ‘printer’ IP address (remote device). So you just have to change part of the IP for the printer’s address.
IP Address Range
The portscanner code is initialized with the device IP and then uses a class C conversion to define start and end address. This will not work if you have a class B or class A network. So, there are possible improvements of the code.
Class c Network
...
PortScanner(Handler handler, String startIP){
mHandler=handler;
m_sStartIP=startIP;
String[] ss = m_sStartIP.split("\\.");
if(ss.length!=4){
state=eState.finished;
msg = mHandler.obtainMessage(hgo.ipprint4.msgTypes.MESSAGE_TOAST);
bundle = new Bundle();
bundle.putString(hgo.ipprint4.msgTypes.TOAST, "inavlid IP");
msg.setData(bundle);
mHandler.sendMessage(msg);
return;
}
bValidIP=true;
baseIP=ss[0]+"."+ss[1]+"."+ss[2];
state=eState.idle;
}
...
As we want to print to a socket, we needed to change the bluetooth connection code to connect to a network socket.
...
private class ConnectThread extends Thread {
private Socket mmSocket=null;
private InetAddress serverIP;
private SocketAddress socketAddress=null;
@TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1)
public ConnectThread(String sIPremote) {
_IPaddr=sIPremote;
try {
addText("get host IP");
serverIP=InetAddress.getByName(_IPaddr);
socketAddress=new InetSocketAddress(serverIP, socketPort);
}catch (UnknownHostException e){
Log.e(TAG, "ConnectThread create() failed", e);
}
catch (IOException e) {
Log.e(TAG, "ConnectThread create() failed", e);
}
}
@Override
public void run() {
Log.i(TAG, "ConnectThread::run()");
setName("ConnectThread");
Socket tmp = null;
try {
addText("new Socket()...");
tmp=new Socket();
tmp.connect(socketAddress, iTimeOut);
addText("new socket() done");
mmSocket=tmp;
}
catch(IllegalArgumentException e){
addText("IllegalArgumentException: " + e.getMessage());
connectionFailed();
addText("Connect failed");
if(mmSocket!=null) {
try {
mmSocket.close();
tmp = null;
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
}
return;
}
catch (IOException e){
addText("IOException: " + e.getMessage());
connectionFailed();
addText("Connect failed");
if(mmSocket!=null) {
try {
mmSocket.close();
tmp = null;
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
}
return;
}
catch (Exception e) {
connectionFailed();
addText("Connect failed");
if(mmSocket!=null) {
try {
mmSocket.close();
tmp = null;
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
}
return;
}
...
Then, we need a stream to write and one to listen. Same as in btPrint4.
...
private class ConnectedThread extends Thread {
private final Socket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(Socket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
...
And then, we can write to the socket.
...
public void write(byte[] buffer) {
addText("write...");
try {
mmOutStream.write(buffer);
mHandler.obtainMessage(msgTypes.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
addText("write done");
}
...
The remaining code of ipPrintFile is similar to the one in btPrint4 and used to manage all the threads used.
Full source code can be downloaded from github.