/* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project
* -----------------------------------------------------------
* LICENSE: http://git.io/vki47 | TERMS: http://git.io/vki4o
* -----------------------------------------------------------
*/
package com.secupwn.aimsicd.utils.atcmd;
import android.os.Message;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/*package*/
class TtyStream extends AtCommandTerminal {
protected InputStream mInputStream;
protected OutputStream mOutputStream;
private boolean mThreadRun = true;
private Thread mIoThread;
protected BlockingQueue<Pair<byte[], Message>> mWriteQ;
/*package*/ TtyStream(InputStream in, OutputStream out) {
mInputStream = in;
mOutputStream = out;
mIoThread = new Thread(new IoRunnable(), "AtCommandTerminalIO");
mIoThread.start();
mWriteQ = new LinkedBlockingQueue<>();
// return result codes, return verbose codes, no local echo
this.send("ATQ0V1E0", null);
}
private class IoRunnable implements Runnable {
@Override
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(mInputStream, "ASCII"));
while (mThreadRun) {
// wait for something to write
byte[] bytesOut;
Message resultMessage;
try {
Pair<byte[], Message> p = mWriteQ.take();
bytesOut = p.first;
resultMessage = p.second;
} catch (InterruptedException e) {
continue; // restart loop
}
try {
mOutputStream.write(bytesOut);
mOutputStream.write('\r');
mOutputStream.flush();
} catch (IOException e) {
log.error("Output IOException", e);
if (resultMessage != null) {
resultMessage.obj = e;
resultMessage.sendToTarget();
}
return; // kill thread
}
/**
* ETSI TS 127 007 gives this example:
* <CR><LF>+CMD2: 3,0,15,"GSM"<CR><LF>
* <CR><LF>+CMD2: (0-3),(0,1),(0-12,15),("GSM","IRA")<CR><LF>
* <CR><LF>OK<CR><LF>
*
* I see embedded <CR><LF> sequences to line-break within responses.
* We can fake it using the BufferedReader, ignoring blank lines.
*/
// TODO error case (on Qcom at least): this thread gets hung waiting for a
// response if garbage commands are issued (e.g., "ls", which I was using to
// make sure shell commands were removed.) It seems local echo will not echo
// that back, but it will echo back valid commands right away (even slow ones
// like AT+COPS=?). So maybe enable echo and see if we get a quick echo back.
// If not, assume the command was not recognized and bail out of the read loop
// below.
// We could also have a timeout where we hope/assume a response isn't coming,
// and move on to process the next command.
// dispatch response lines until done
String line;
List<String> lines = new ArrayList<>();
do {
try {
line = in.readLine();
if (line == null) {
throw new IOException("reader closed");
}
} catch (IOException e) {
log.error("Input IOException", e);
if (resultMessage != null) {
resultMessage.obj = e;
resultMessage.sendToTarget();
}
return; // kill thread
}
if (line.length() != 0) {
lines.add(line);
}
// ignore empty lines
}
while (!(line.equals("OK") || line.equals("ERROR") || line.startsWith("+CME ERROR")));
// XXX this logging could have sensitive info
//log.debug("IO< " + lines);
if (resultMessage != null) {
resultMessage.obj = lines;
resultMessage.sendToTarget();
} else {
log.debug("Data came in with no handler");
}
}
} catch (UnsupportedEncodingException e) {
// ASCII should work
throw new RuntimeException(e);
} finally {
dispose();
}
}
}
@Override
public void send(String s, Message resultMessage) {
try {
// XXX this logging could have sensitive info
//log.debug("IO> " + s);
mWriteQ.add(Pair.create(s.getBytes("ASCII"), resultMessage));
} catch (UnsupportedEncodingException e) {
// we assume that if a String is being used for convenience, it must be ASCII
throw new RuntimeException(e);
}
}
@Override
public void dispose() {
mThreadRun = false;
}
}