view src/com/five_ten_sg/connectbot/monitor/MonitorService.java @ 26:3a5df66c0480

Added tag stable-1.0.4-0 for changeset 3975d341e3dd
author Carl Byington <carl@five-ten-sg.com>
date Fri, 01 May 2015 12:34:17 -0700
parents 4f1cc4f44c41
children
line wrap: on
line source

package com.five_ten_sg.connectbot.monitor;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Locale;

import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.wifi.WifiManager.WifiLock;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.util.Log;
import android.widget.TextView;

public class MonitorService extends Service implements OnInitListener {
    public final static String TAG = "ConnectBot.MonitorService";

    public  static final char MONITOR_CMD_INIT          = 0;
    public  static final char MONITOR_CMD_ACTIVATE      = 1;
    public  static final char MONITOR_CMD_KEYSTATE      = 2;
    public  static final char MONITOR_CMD_CURSORMOVE    = 3;
    public  static final char MONITOR_CMD_SCREENCHANGE  = 4;
    public  static final char MONITOR_CMD_FIELDVALUE    = 5;
    public  static final char MONITOR_CMD_SETFIELD      = 5;
    public  static final char MONITOR_CMD_GETFIELD      = 6;
    public  static final char MONITOR_CMD_SCREENWATCH   = 7;
    public  static final char MONITOR_CMD_DEPRESS       = 8;
    public  static final char MONITOR_CMD_SHOWURL       = 9;
    public  static final char MONITOR_CMD_SWITCHSESSION = 10;
    public  static final char MONITOR_CMD_CURSORREQUEST = 11;

    public  static final char CURSOR_REQUESTED      = 0;
    public  static final char CURSOR_SCREEN_CHANGE  = 1;
    public  static final char CURSOR_USER_KEY       = 2;

    public static final int       MONITORPORT = 6000;
    public static ConcurrentHashMap<Integer,CommunicationThread> clients = new ConcurrentHashMap<Integer,CommunicationThread>();
    public static int             currentConnection = -1;

    private boolean               speech  = false;
    private TextToSpeech          talker  = null;
    private BlockingQueue<String> talkerQueue = null;
    public  Handler               handler = null;
    private ServerSocket          serverSocket;
    private Thread                serverThread = null;
    private WifiManager.WifiLock  wifiLock;
    private PowerManager.WakeLock wakeLock;
    final private IBinder         binder = new MonitorBinder();


    public class MonitorBinder extends Binder {
        public MonitorService getService() {
            return MonitorService.this;
        }
    }

    @Override
    public void onInit(int status) {
        if (status == TextToSpeech.SUCCESS) {
            talker.setLanguage(Locale.US);
            speech = true;
        }
    }

    @Override
    public void onCreate() {
        WifiManager wMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        wifiLock = wMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, "MyWifiLock");
        wifiLock.acquire();

        PowerManager pMgr = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = pMgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakeLock");
        wakeLock.acquire();

        talker = new TextToSpeech(this, this);
        this.serverThread = new Thread(new ServerThread());
        this.serverThread.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        startService(new Intent(this, MonitorService.class));
        return binder;
    }

    public void printer(String msg) {
        if (handler != null) handler.sendMessage(handler.obtainMessage(MonitorActivity.MESSAGE_CODE_PRINT, msg));
    }

    public synchronized void setCurrentConnection(int connection) {
        currentConnection = connection;
    }

    public synchronized int nextConnection() {
        int c = 1;
        while (clients.get(c) != null) c++;
        return c;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "service onStartCommand()");
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        try {
            Log.i(TAG, "service onDestroy()");
            teCloseAll();
            talker.stop();
            talker.shutdown();
            wifiLock.release();
            wakeLock.release();
            serverSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "exception in onDestroy()", e);
        }
        super.onDestroy();
    }

    class ServerThread extends Thread {
        public void run() {
            Socket  socket = null;
            int connection = 0;
            try {
                serverSocket = new ServerSocket(MONITORPORT);
            } catch (IOException e) {
                Log.e(TAG, "exception in ServerThread.run(), cannot create listening socket", e);
                return;
            }
            while (true) {
                try{
                   socket     = serverSocket.accept();
                   connection = nextConnection();
                   CommunicationThread commThread = new CommunicationThread(connection, socket);
                   clients.put(connection, commThread);
                   commThread.start();
                } catch (IOException e) {
                   Log.e(TAG, "exception in ServerThread.run(), listening socket closed", e);
                   break;
                }
            }
        }
    }

    class triple {
        private int l, c;
        private char[] b;
        public triple(int l, int c, char[] b) {
            this.l = l;
            this.c = c;
            this.b = b;
        }
    }

    class CommunicationThread extends Thread {
        public  int            thread_id;
        public  int            connection;
        private String         initString = null;
        private Socket         client_socket;
        private InputStream    client_in;
        private OutputStream   client_out;
        private boolean        is_closing = false;
        private Integer        getfields_outstanding = 0;
        private BlockingQueue<triple>  value_queue = new ArrayBlockingQueue<triple>(1);
        private BlockingQueue<char[]>  packet_queue = new ArrayBlockingQueue<char[]>(10000);

        public CommunicationThread(int handle, Socket socket) {
            connection    = handle;
            client_socket = socket;
            try {
                client_in  = client_socket.getInputStream();
                client_out = client_socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "exception in CommunicationThread() constructor, cannot get socket streams", e);
            }
        }

        public synchronized void abandon() {
            value_queue.offer(new triple(0, 0, new char[0]));
        }

        public void speak(byte [] msg, boolean flush, boolean synchronous) {
            if (speech) {
                String smsg = bytesToString(msg);
                if (synchronous) {
                    HashMap<String, String> myHashParms = new HashMap();
                    myHashParms.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, String.format("connection %d", connection));
                	talker.speak(smsg, (flush) ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD, myHashParms);
                    try {
                        String x = talkerQueue.take(); // wait for completion
                    } catch (InterruptedException e) {
                        Log.e(TAG, "exception in cm.speak()", e);
                    }
                }
                else {
            	    talker.speak(smsg, (flush) ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD, null);
                }
            }
        }

        public String bytesToString(byte[] b) {
            char[] c = new char[b.length];
            int bp = 0;
            for(int i = 0; i < c.length; i++) {
                byte b1 = 0;
                byte b2 = b[bp++];
                c[i] = (char) (((b1 & 0x00FF) << 8) + (b2 & 0x00FF));
            }
            return new String(c);
        }

        public char[] bytesToChars(byte[] b, int len) {
            char[] c = new char[len >> 1];
            int bp = 0;
            for(int i = 0; i < c.length; i++) {
                byte b1 = b[bp++];
                byte b2 = b[bp++];
                c[i] = (char) (((b1 & 0x00FF) << 8) + (b2 & 0x00FF));
            }
            return c;
        }

        public byte[] charsToBytes(char[] c) {
            byte[] b = new byte[c.length << 1];
            int bp = 0;
            for (int i=0; i<c.length; i++) {
                b[bp++] = (byte) ((c[i] & 0xff00) >> 8);
                b[bp++] = (byte)  (c[i] & 0x00ff);
            }
            return b;
        }

        void cleanup(char[] buf) {
            int i;
            for (i=0; i<buf.length; i++) {
                if ((int)(buf[i]) < 32) buf[i] = ' ';
            }
        }

        public int cmGetField(char[] c) {
            int request;
            synchronized(getfields_outstanding) {
                request = getfields_outstanding;
                getfields_outstanding = getfields_outstanding + 1;
                value_queue.clear();  // we never have more than one outstanding getfield request from the reco thread on the connection
                clientWrite(MONITOR_CMD_GETFIELD, c);
            }
            return request;
        }

        public synchronized void clientWrite(char cmd, char[] c) {
            try {
                if (client_out != null) {
                    c[0] = (char)(c.length - 1);    // number of chars following
                    c[1] = cmd;
                    Log.i(TAG, String.format("sending %d command", (int)cmd));
                    client_out.write(charsToBytes(c));
                    client_out.flush();
                }
            }
            catch (IOException e) {
                Log.e(TAG, "exception in monitorWrite()", e);
                try {
                    client_out.close();
                }
                catch (IOException ee) {
                    Log.e(TAG, "exception in monitorWrite() closing socket", ee);
                }
                client_out = null;
            }
        };

        private char[] forceRead(int len) throws IOException {
            int len2 = len*2;
            int off  = 0;
            byte[] b = new byte[len2];
            while (off < len2) {
                int l = client_in.read(b, off, len2-off);
                if (l < 0) {
                    is_closing = true;
                    throw new IOException("eof");
                }
                off += l;
            }
            return bytesToChars(b, len2);
        }

        public char[] readPacket() throws IOException {
            char[] len = forceRead(1);
            return forceRead(len[0]);
        }

        public char[] nextPacket() throws IOException, InterruptedException {
            char[] packet = packet_queue.poll();
            if (packet == null) {
                packet = readPacket();
                if (packet[0] == MONITOR_CMD_FIELDVALUE) {
                    synchronized(getfields_outstanding) {
                        getfields_outstanding = getfields_outstanding - 1;
                    }
                }
            }
            return packet;
        }

        private triple reformatValue(char[] packet) {
            int plen = packet.length;
            char[] buf = new char[plen-3];
            System.arraycopy(packet, 3, buf, 0, plen-3);
            cleanup(buf);
            Log.i(TAG, String.format("teFieldValue %d line %d column %d b.len %d", connection, (int)packet[1], (int)packet[2], buf.length));
            return new triple(packet[1], packet[2], buf);
        }

        public triple peekValue(int request) {
            try {
                while (true) {
                    char[] packet = readPacket();
                    if (packet[0] == MONITOR_CMD_FIELDVALUE) {
                        synchronized(getfields_outstanding) {
                            getfields_outstanding = getfields_outstanding - 1;
                            if (request == 0) {
                                return reformatValue(packet);
                            }
                            else {
                                packet_queue.put(packet);
                                request = request - 1;
                            }
                        }
                    }
                    else {
                        packet_queue.put(packet);
                    }
                }
            } catch (IOException e) {
                return new triple(0, 0, new char[0]);
            } catch (InterruptedException e) {
                return new triple(0, 0, new char[0]);
            }
        }

        public void run() {
            thread_id = android.os.Process.myTid();
            Log.i(TAG, String.format("CommunicationThread.run() client %d connected", connection));
            while (true) {
                try {
                    char[] packet = nextPacket();
                    char[] buf;
                    char cmd = packet[0];
                    int plen = packet.length;
                    //Log.i(TAG, String.format("received %d command length %d", (int)cmd, plen));
                    switch (cmd) {
                        case MONITOR_CMD_INIT:
                            buf = new char[plen-1];
                            System.arraycopy(packet, 1, buf, 0, plen-1);
                            abandonGetField(connection);
                            initString = new String(buf);
                            teInit(connection, initString);
                            break;
                        case MONITOR_CMD_ACTIVATE:
                            abandonGetField(connection);
                            buf = new char[plen-3];
                            System.arraycopy(packet, 3, buf, 0, plen-3);
                            teActivate(connection, initString, packet[1], packet[2], buf);
                            break;
                        case MONITOR_CMD_KEYSTATE:
                            teKeyState(connection, (packet[1] == 1));
                            break;
                        case MONITOR_CMD_CURSORMOVE:
                            teCursorMove(connection, packet[1], packet[2], packet[3]);
                            break;
                        case MONITOR_CMD_SCREENCHANGE:
                            buf = new char[plen-3];
                            System.arraycopy(packet, 3, buf, 0, plen-3);
                            cleanup(buf);
                            teScreenChange(connection, packet[1], packet[2], buf);
                            break;
                        case MONITOR_CMD_FIELDVALUE:
                            value_queue.clear();
                            value_queue.put(reformatValue(packet));
                            break;
                        default:
                            break;
                    }
                } catch (IOException e) {
                    if (!is_closing) Log.e(TAG, "exception in CommunicationThread.run()", e);
                    break;
                } catch (InterruptedException e) {
                    Log.e(TAG, "exception in CommunicationThread.run()", e);
                    break;
                }
            }
            Log.i(TAG, String.format("shutting down connection %d", connection));
            try {
                if (client_in     != null) client_in.close();
                if (client_out    != null) client_out.close();
                if (client_socket != null) client_socket.close();
            } catch (IOException e) {
                Log.e(TAG, "exception in CommunicationThread.run() closing sockets", e);
            }
            client_in     = null;
            client_out    = null;
            client_socket = null;
            clients.remove(connection);
        }
    }

    private void abandonGetField(int except) {
        for (CommunicationThread cm : clients.values()) {
            if (cm.connection != except) {
                cm.abandon();
            }
        }
    }


    ////////////////////////////////////////
    //// these functions run on the reader thread here and call your monitoring code

    public void teInit(int connection, String fn) {
        Log.i(TAG, String.format("teInit %d file %s", connection, fn));
        printer(String.format("init %d %s", connection, fn));
        setCurrentConnection(connection);
    }

    public void teCloseAll() {
        Log.i(TAG, String.format("teCloseAll"));
    }

    public void teClose(int connection) {
        Log.i(TAG, String.format("teClose %d", connection));
        setCurrentConnection(-1);
    }

    public void teActivate(int connection, String fn, int lines, int columns, char[] buf) {
        Log.i(TAG, String.format("teActivate %d", connection));
        printer(String.format("activate %d lines %d columns %d b.len %d", connection, lines, columns, buf.length));
        boolean sameinit = false;
        CommunicationThread cm = clients.get(currentConnection);
        if (cm != null) {
            sameinit = (cm.initString == fn);
        }
        setCurrentConnection(connection);
    }

    public void teKeyState(int connection, boolean down) {
        String d = (down) ? "yes" : "no";
        Log.i(TAG, String.format("teKeyState %d isdown %s", connection, d));
        printer(String.format("keystate %d isdown %s", connection, d));
    }

    public void teCursorMove(int connection, int l, int c, int why) {
        Log.i(TAG, String.format("teCursorMove %d line %d column %d why %d", connection, l, c, why));
    }

    public void teScreenChange(int connection, int lines, int columns, char[] buf) {
        Log.i(TAG, String.format("teScreenChange %d lines %d columns %d b.len %d", connection, lines, columns, buf.length));
    }


    ////////////////////////////////////////
    //// these functions are called from your monitoring code thread

    public static void teSetField(int connection, int l, int c, char[] buf) {
        int len = buf.length;
        Log.i(TAG, String.format("teSetField %d request line %d column %d len %d", connection, l, c, len));
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char[] arg2 = new char[4 + len];
            arg2[2] = (char) (l & 0x0000ffff);
            arg2[3] = (char) (c & 0x0000ffff);
            int base = 4;
            System.arraycopy(buf, 0, arg2, base, len);
            cm.clientWrite(MONITOR_CMD_SETFIELD, arg2);
        }
    }

    public static char[] teGetField(int connection, int l, int c, int len) {
        Log.i(TAG, String.format("teGetField %d request line %d column %d len %d", connection, l, c, len));
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char[] arg = new char[5];
            arg[2] = (char) (l & 0x0000ffff);
            arg[3] = (char) (c & 0x0000ffff);
            arg[4] = (char) (len & 0x0000ffff);
            int request = cm.cmGetField(arg);
            try {
                int tid = android.os.Process.myTid();
                triple t;
                if (tid == cm.thread_id) {
                    // we are running on the socket reader thread, called via teCursorMove() or teScreenChange()
                    // we need to peek command packets from the socket looking for our fieldvalue response
                    Log.i(TAG, String.format("java teGetField() peeking value for getfield on reader thread"));
                    t = cm.peekValue(request);
                }
                else {
                    // we are running on some other thread, just wait for the reader thread to
                    // process the fieldvalue and put it on the queue.
                    Log.i(TAG, String.format("java teGetField() waiting for data for getfield on reco thread"));
                    t = cm.value_queue.take(); // wait for response
                }
                Log.i(TAG, String.format("teGetField %d response line %d column %d len %d", connection, t.l, t.c, t.b.length));
                return t.b;
            } catch (InterruptedException e) {
                Log.e(TAG, "exception in teGetField(), return empty string", e);
            }
        }
        return new char[0];
    }

    public static void teScreenWatch(int connection, int l, int c, int len) {
        Log.i(TAG, String.format("teScreenWatch %d request line %d column %d len %d", connection, l, c, len));
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char[] arg = new char[5];
            arg[2] = (char) (l & 0x0000ffff);
            arg[3] = (char) (c & 0x0000ffff);
            arg[4] = (char) (len & 0x0000ffff);
            cm.clientWrite(MONITOR_CMD_GETFIELD, arg);
        }
    }

    public static void teSpeak(int connection, byte [] msg, boolean flush, boolean synchronous) {
        CommunicationThread cm = clients.get(connection);
        if (cm != null) cm.speak(msg, flush, synchronous);
    }

    public static void teDepress(int connection, int vk_key) {
        // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731
        Log.i(TAG, String.format("teDepress %d, %d", connection, vk_key));
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char[] arg = new char[3];
            arg[2] = (char) (vk_key & 0x0000ffff);
            cm.clientWrite(MONITOR_CMD_DEPRESS, arg);
        }
    }

    public static void teShowUrl(int connection, char [] url) {
        int len = url.length;
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char[] arg2 = new char[2 + len];
            int base = 2;
            System.arraycopy(url, 0, arg2, base, len);
            cm.clientWrite(MONITOR_CMD_SHOWURL, arg2);
        }
    }

    public static void teAbandonGetField(int connection) {
        Log.i(TAG, String.format("teAbandonGetField %d", connection));
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            cm.abandon();
        }
    }

    public static void teSwitchSession(int connection) {
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char [] arg2 = new char[2];
            cm.clientWrite(MONITOR_CMD_SWITCHSESSION, arg2);
        }
    }

    public static void teCursorRequest(int connection) {
        CommunicationThread cm = clients.get(connection);
        if (cm != null) {
            char [] arg2 = new char[2];
            cm.clientWrite(MONITOR_CMD_CURSORREQUEST, arg2);
        }
    }

}