view src/com/five_ten_sg/connectbot/transport/TN5250.java @ 184:cdaaa53b5eca

setfield positions the cursor properly as if the characters were typed
author Carl Byington <carl@five-ten-sg.com>
date Wed, 02 Jul 2014 14:32:13 -0700
parents 2a7199ad90be
children c51bcf9f0516
line wrap: on
line source

/*
 * 510ConnectBot
 * Copyright 2014 Carl Byington
 *
 * 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
 *
 *     http://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.
 */

package com.five_ten_sg.connectbot.transport;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.tn5250j.framework.tn5250.Screen5250;
import org.tn5250j.framework.tn5250.tnvt;

import com.five_ten_sg.connectbot.R;
import com.five_ten_sg.connectbot.bean.HostBean;
import com.five_ten_sg.connectbot.bean.PortForwardBean;
import com.five_ten_sg.connectbot.service.TerminalBridge;
import com.five_ten_sg.connectbot.service.TerminalKeyListener;
import com.five_ten_sg.connectbot.service.TerminalManager;
import com.five_ten_sg.connectbot.util.HostDatabase;
import com.five_ten_sg.connectbot.util.PreferenceConstants;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import de.mud.terminal.vt320;


/**
 * @author Carl Byington
 *
 */
public class TN5250 extends AbsTransport {
    private static final String PROTOCOL = "tn5250";
    private static final String TAG = "ConnectBot.tn5250";
    private static final int DEFAULT_PORT = 23;

    private Screen5250 screen52;
    private tnvt       handler = null;
    private Socket     socket;
    private boolean    connected = false;

    static final Pattern hostmask;
    static {
        hostmask = Pattern.compile("^([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
    }


    class vt320x5250 extends vt320 {
        private HashMap<Integer, Integer> controls;
        private HashMap<Integer, String> mnemonics;

        public vt320x5250() {
            this(80, 24);
        }

        public vt320x5250(int width, int height) {
            super(width, height);
            controls = new HashMap<Integer, Integer>();
            controls.put(0x01, KEY_PAUSE);      // ctrl-a -> [attn]
            controls.put(0x08, KEY_BACK_SPACE);
            controls.put(0x09, KEY_TAB);
            controls.put(0x0d, KEY_ENTER);
            controls.put(0x12, KEY_ESCAPE);     // ctrl-r -> [reset]
            controls.put(0x13, KEY_SYSREQ);     // ctrl-s -> [sysreq]
            controls.put(0x1b, KEY_ESCAPE);     // esc    -> [reset]
            mnemonics = new HashMap<Integer, String>();
            mnemonics.put(KEY_PAUSE       , "[attn]");
            mnemonics.put(KEY_F1          , "[pf1]");
            mnemonics.put(KEY_F2          , "[pf2]");
            mnemonics.put(KEY_F3          , "[pf3]");
            mnemonics.put(KEY_F4          , "[pf4]");
            mnemonics.put(KEY_F5          , "[pf5]");
            mnemonics.put(KEY_F6          , "[pf6]");
            mnemonics.put(KEY_F7          , "[pf7]");
            mnemonics.put(KEY_F8          , "[pf8]");
            mnemonics.put(KEY_F9          , "[pf9]");
            mnemonics.put(KEY_F10         , "[pf10]");
            mnemonics.put(KEY_F11         , "[pf11]");
            mnemonics.put(KEY_F12         , "[pf12]");
            mnemonics.put(KEY_F13         , "[pf13]");
            mnemonics.put(KEY_F14         , "[pf14]");
            mnemonics.put(KEY_F15         , "[pf15]");
            mnemonics.put(KEY_F16         , "[pf16]");
            mnemonics.put(KEY_F17         , "[pf17]");
            mnemonics.put(KEY_F18         , "[pf18]");
            mnemonics.put(KEY_F19         , "[pf19]");
            mnemonics.put(KEY_F20         , "[pf20]");
            mnemonics.put(KEY_F21         , "[pf21]");
            mnemonics.put(KEY_F22         , "[pf22]");
            mnemonics.put(KEY_F23         , "[pf23]");
            mnemonics.put(KEY_F24         , "[pf24]");
            mnemonics.put(KEY_UP          , "[up]");
            mnemonics.put(KEY_DOWN        , "[down]");
            mnemonics.put(KEY_LEFT        , "[left]");
            mnemonics.put(KEY_RIGHT       , "[right]");
            mnemonics.put(KEY_PAGE_DOWN   , "[pgdown]");
            mnemonics.put(KEY_PAGE_UP     , "[pgup]");
            mnemonics.put(KEY_INSERT      , "[insert]");
            mnemonics.put(KEY_DELETE      , "[delete]");
            mnemonics.put(KEY_BACK_SPACE  , "[backspace]");
            mnemonics.put(KEY_HOME        , "[home]");
            mnemonics.put(KEY_END         , "[end]");
            mnemonics.put(KEY_NUM_LOCK    , "");
            mnemonics.put(KEY_CAPS_LOCK   , "");
            mnemonics.put(KEY_SHIFT       , "");
            mnemonics.put(KEY_CONTROL     , "");
            mnemonics.put(KEY_ALT         , "");
            mnemonics.put(KEY_ENTER       , "[enter]");
            mnemonics.put(KEY_NUMPAD0     , "0");
            mnemonics.put(KEY_NUMPAD1     , "1");
            mnemonics.put(KEY_NUMPAD2     , "2");
            mnemonics.put(KEY_NUMPAD3     , "3");
            mnemonics.put(KEY_NUMPAD4     , "4");
            mnemonics.put(KEY_NUMPAD5     , "5");
            mnemonics.put(KEY_NUMPAD6     , "6");
            mnemonics.put(KEY_NUMPAD7     , "7");
            mnemonics.put(KEY_NUMPAD8     , "8");
            mnemonics.put(KEY_NUMPAD9     , "9");
            mnemonics.put(KEY_DECIMAL     , ".");
            mnemonics.put(KEY_ADD         , "+");
            mnemonics.put(KEY_ESCAPE      , "[reset]");
            mnemonics.put(KEY_TAB         , "[tab]");
            mnemonics.put(KEY_SYSREQ      , "[sysreq]");
        }

        @Override
        public void debug(String s) {
            Log.d(TAG, s);
        }

        // monitor injecting a field
        @Override
        public void setField(int l, int c, char [] data) {
            screen52.setField(l, c, data);
        }

        // monitor simulating key depress
        @Override
        public void keyDepressed(int keyCode, char keyChar, int modifiers) {
            if (mnemonics.containsKey(keyCode)) {
                String s = mnemonics.get(keyCode);
                if (s != "") screen52.sendKeys(s);
            }
        }

        // terminal key listener found special key, send notification to monitor
        @Override
        public void monitorKey(boolean down) {
            if (bridge.monitor != null) bridge.monitor.keyState(down);
        }

        // terminal key listener sending to local screen
        @Override
        public void write(byte[] b) {
            screen52.sendKeys(new String(b));
            testChanged();
        }
        @Override
        public void write(int b) {
            if (b == 120) {
                // special case x for testing
                setField(17, 44, new char[] { '1', '2' });
                keyPressed(KEY_TAB, ' ', 0);
                keyPressed(KEY_TAB, ' ', 0);
                setField(-1, -1, new char[] { '3', '4' });
                setField(18, 44, new char[0]);
                setField(-1, -1, new char[] { '5', '6' });
                return;
            }
            if (b == 121) {
                // special case y for testing
                setField(17, 44, new char[0]);
                setField(-1, -1, new char[] { '2', '1' });
                keyPressed(KEY_TAB, ' ', 0);
                keyPressed(KEY_TAB, ' ', 0);
                setField(-1, -1, new char[] { '4', '3' });
                setField(18, 44, new char[0]);
                setField(-1, -1, new char[] { '6', '5' });
                return;
            }
            if (b == 122) {
                // special case z for testing
                setField(17, 40, new char[0]);
                return;
            }
            if (controls.containsKey(b)) keyPressed(controls.get(b), ' ', 0);
            else                         screen52.sendKeys(new String(new byte[] {(byte)b}));
            testChanged();
        }
        @Override
        public void keyPressed(int keyCode, char keyChar, int modifiers) {
            keyDepressed(keyCode, keyChar, modifiers);
            testChanged();
        }

        // 5250 writing to the screen
        // test for changed screen contents
        @Override
        public void testChanged() {
            if (bridge.monitor != null) bridge.monitor.testChanged();
        }
        @Override
        public void putChar(int c, int l, char ch, int attributes) {
            if (bridge.monitor != null) bridge.monitor.screenChanged(l, c);

            super.putChar(c, l, ch, attributes);
        }
        @Override
        public void setCursorPosition(int c, int l) {
            if (bridge.monitor != null) bridge.monitor.cursorMove(l, c);

            super.setCursorPosition(c, l);
            redraw();
        }
    };


    public TN5250() {
        super();
    }


    /**
     * @return protocol part of the URI
     */
    public static String getProtocolName() {
        return PROTOCOL;
    }


    /**
     * Encode the current transport into a URI that can be passed via intent calls.
     * @return URI to host
     */
    public Uri getUri(String input) {
        Matcher matcher = hostmask.matcher(input);

        if (!matcher.matches())
            return null;

        StringBuilder sb = new StringBuilder();
        sb.append(PROTOCOL)
        .append("://")
        .append(matcher.group(1));
        String portString = matcher.group(3);
        int port = DEFAULT_PORT;

        if (portString != null) {
            try {
                port = Integer.parseInt(portString);

                if (port < 1 || port > 65535) {
                    port = DEFAULT_PORT;
                }
            }
            catch (NumberFormatException nfe) {
                // Keep the default port
            }
        }

        if (port != DEFAULT_PORT) {
            sb.append(':');
            sb.append(port);
        }

        sb.append("/#")
        .append(Uri.encode(input));
        Uri uri = Uri.parse(sb.toString());
        return uri;
    }


    /**
     * Causes transport to connect to the target host. After connecting but before a
     * session is started, must call back to {@link TerminalBridge#onConnected()}.
     * After that call a session may be opened.
     */
    @Override
    public void connect() {
        screen52  = new Screen5250();
        handler   = new tnvt(screen52, true, false, bridge, manager);
        screen52.setVT(handler);
        screen52.setBuffer(buffer);
        bridge.addFontSizeChangedListener(screen52);
        connected = handler.connect(host, homeDirectory, buffer);
        if (connected) bridge.onConnected();
    }


    /**
     * Checks if read() will block. If there are no bytes remaining in
     * the underlying transport, return true.
     */
    @Override
    public boolean willBlock() {
        // we don't use a relay thread between the transport and the vt320 buffer
        return true;
    }


    /**
     * Reads from the transport. Transport must support reading into a byte array
     * <code>buffer</code> at the start of <code>offset</code> and a maximum of
     * <code>length</code> bytes. If the remote host disconnects, throw an
     * {@link IOException}.
     * @param buffer byte buffer to store read bytes into
     * @param offset where to start writing in the buffer
     * @param length maximum number of bytes to read
     * @return number of bytes read
     * @throws IOException when remote host disconnects
     */
    public int read(byte[] buffer, int offset, int length) throws IOException {
        // we don't use a relay thread between the transport and the vt320 buffer
        return 0;
    }


    /**
     * Writes to the transport. If the host is not yet connected, simply return without
     * doing anything. An {@link IOException} should be thrown if there is an error after
     * connection.
     * @param buffer bytes to write to transport
     * @throws IOException when there is a problem writing after connection
     */
    public void write(byte[] buffer) throws IOException {
    }


    /**
     * Writes to the transport. See {@link #write(byte[])} for behavior details.
     * @param c character to write to the transport
     * @throws IOException when there is a problem writing after connection
     */
    public void write(int c) throws IOException {
    }


    /**
     * Flushes the write commands to the transport.
     * @throws IOException when there is a problem writing after connection
     */
    public void flush() throws IOException {
    }


    /**
     * Closes the connection to the terminal.
     */
    public void close() {
        handler.disconnect();
        connected = false;
        bridge.removeFontSizeChangedListener(screen52);
        bridge.dispatchDisconnect(false);
    }


    /**
     * Tells the transport what dimensions the display is currently
     * @param columns columns of text
     * @param rows rows of text
     * @param width width in pixels
     * @param height height in pixels
     */
    @Override
    public void setDimensions(int columns, int rows, int width, int height) {
        // do nothing
    }


    @Override
    public vt320 getTransportBuffer() {
        buffer = new vt320x5250();
        return setupTransportBuffer();
    }


    @Override
    public int getDefaultPort() {
        return DEFAULT_PORT;
    }


    @Override
    public boolean isConnected() {
        return connected;
    }


    @Override
    public boolean isSessionOpen() {
        return connected;
    }


    @Override
    public boolean isAuthenticated() {
        return connected;
    }


    @Override
    public String getDefaultNickname(String username, String hostname, int port) {
        if (port == DEFAULT_PORT) {
            return String.format("%s", hostname);
        }
        else {
            return String.format("%s:%d", hostname, port);
        }
    }


    @Override
    public void getSelectionArgs(Uri uri, Map<String, String> selection) {
        selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
        selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
        selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
        int port = uri.getPort();

        if (port < 0) port = DEFAULT_PORT;

        selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
    }


    @Override
    public HostBean createHost(Uri uri) {
        HostBean host = new HostBean();
        host.setProtocol(PROTOCOL);
        host.setHostname(uri.getHost());
        int port = uri.getPort();

        if (port < 0) port = DEFAULT_PORT;

        host.setPort(port);
        String nickname = uri.getFragment();

        if (nickname == null || nickname.length() == 0) {
            nickname = getDefaultNickname(host.getUsername(), host.getHostname(), host.getPort());
        }
        host.setNickname(nickname);

        return host;
    }


    public String getFormatHint(Context context) {
        return String.format("%s:%s",
                             context.getString(R.string.format_hostname),
                             context.getString(R.string.format_port));
    }


    @Override
    public boolean usesNetwork() {
        return true;
    }


    @Override
    public boolean needsRelay() {
        // we don't use a relay thread between the transport and the vt320 buffer
        return false;
    }

    public TerminalKeyListener getTerminalKeyListener() {
        return new TerminalKeyListener(manager, bridge, buffer, host.getEncoding());
    }

}