view src/com/five_ten_sg/connectbot/transport/TN5250.java @ 50:2cd3d8091e37 tn5250

start tn5250 integration
author Carl Byington <carl@five-ten-sg.com>
date Wed, 11 Jun 2014 11:50:25 -0700
parents 8887bff45dee
children 8c6de858bb73
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.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.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 {
        @Override
        public void debug(String s) {
            Log.d(TAG, s);
        }
        @Override
        public void write(byte[] b) {
            screen52.sendKeys(new String(b));
        }
        @Override
        public void write(int b) {
            screen52.sendKeys(new String(new byte[] {(byte)b}));
        }
        // bridge.monitor placement of new characters
        @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);
        }
    };


    class Terminal5250KeyListener extends TerminalKeyListener {
        public Terminal5250KeyListener(TerminalManager manager,
                                       TerminalBridge bridge,
                                       vt320 buffer,
                                       String encoding) {
            super(manager, bridge, buffer, encoding);
        }

        /**
         * Handle onKey() events coming down from a {@link com.five_ten_sg.connectbot.TerminalView} above us.
         * Modify the keys to make more sense to a host then pass it to the 5250.
         */
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            try {
                // skip keys if we aren't connected yet or have been disconnected
                if (bridge.isDisconnected()) return false;

                // Ignore all key-up events except for the special keys
                if (event.getAction() == KeyEvent.ACTION_UP) {
                    // There's nothing here for virtual keyboard users.
                    if (!hardKeyboard || hardKeyboardHidden) return false;

                    // if keycode debugging enabled, log and print the pressed key
                    if (prefs.getBoolean(PreferenceConstants.DEBUG_KEYCODES, false)) {
                        String keyCodeString = String.format(": %d", keyCode);
                        String toastText = v.getContext().getString(R.string.keycode_pressed) + keyCodeString;
                        Log.d(TAG, toastText);
                    }

                    if (fullKeyboard()) {
                        switch (keyCode) {
                            case KeyEvent.KEYCODE_CTRL_LEFT:
                            case KeyEvent.KEYCODE_CTRL_RIGHT:
                                metaKeyUp(META_CTRL_ON);
                                return true;

                            case KeyEvent.KEYCODE_ALT_LEFT:
                            case KeyEvent.KEYCODE_ALT_RIGHT:
                                metaKeyUp(META_ALT_ON);
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_LEFT:
                            case KeyEvent.KEYCODE_SHIFT_RIGHT:
                                metaKeyUp(META_SHIFT_ON);
                                return true;

                            default:
                        }
                    }
                    else if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
                        if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
                                && (metaState & META_SLASH) != 0) {
                            metaState &= ~(META_SLASH | META_TRANSIENT);
                            buffer.write('/');
                            return true;
                        }
                        else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
                                 && (metaState & META_TAB) != 0) {
                            metaState &= ~(META_TAB | META_TRANSIENT);
                            buffer.write("[tab]");
                            return true;
                        }
                    }
                    else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
                        if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
                                && (metaState & META_SLASH) != 0) {
                            metaState &= ~(META_SLASH | META_TRANSIENT);
                            buffer.write('/');
                            return true;
                        }
                        else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
                                 && (metaState & META_TAB) != 0) {
                            metaState &= ~(META_TAB | META_TRANSIENT);
                            buffer.write("[tab]");
                            return true;
                        }
                    }

                    return false;
                }

                bridge.resetScrollPosition();

                if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
                        event.getAction() == KeyEvent.ACTION_MULTIPLE) {
                    byte[] input = event.getCharacters().getBytes(encoding);
                    buffer.write(input);
                    return true;
                }

                int curMetaState = event.getMetaState();
                final int orgMetaState = curMetaState;

                if ((metaState & META_SHIFT_MASK) != 0) {
                    curMetaState |= KeyEvent.META_SHIFT_ON;
                }

                if ((metaState & META_ALT_MASK) != 0) {
                    curMetaState |= KeyEvent.META_ALT_ON;
                }

                int uchar = event.getUnicodeChar(curMetaState);

                // no hard keyboard?  ALT-k should pass through to below
                if ((orgMetaState & KeyEvent.META_ALT_ON) != 0 &&
                        (!hardKeyboard || hardKeyboardHidden)) {
                    uchar = 0;
                }

                if ((uchar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
                    mDeadKey = uchar & KeyCharacterMap.COMBINING_ACCENT_MASK;
                    return true;
                }

                if (mDeadKey != 0 && uchar != 0) {
                    uchar = KeyCharacterMap.getDeadChar(mDeadKey, uchar);
                    mDeadKey = 0;
                }

                // handle customized keymaps
                if (customKeymapAction(v, keyCode, event))
                    return true;

                if (v != null) {
                    //Show up the CharacterPickerDialog when the SYM key is pressed
                    if ((isSymKey(keyCode) || uchar == KeyCharacterMap.PICKER_DIALOG_INPUT)) {
                        bridge.showCharPickerDialog();

                        if (metaState == 4) { // reset fn-key state
                            metaState = 0;
                            bridge.redraw();
                        }

                        return true;
                    }
                    else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
                        //Show up the URL scan dialog when the search key is pressed
                        urlScan(v);
                        return true;
                    }
                }

                // otherwise pass through to existing session
                // print normal keys
                if (uchar > 0x00 && keyCode != KeyEvent.KEYCODE_ENTER) {
                    metaState &= ~(META_SLASH | META_TAB);
                    // Remove shift and alt modifiers
                    final int lastMetaState = metaState;
                    metaState &= ~(META_SHIFT_ON | META_ALT_ON);

                    if (metaState != lastMetaState) {
                        bridge.redraw();
                    }

                    if ((metaState & META_CTRL_MASK) != 0) {
                        metaState &= ~META_CTRL_ON;
                        bridge.redraw();

                        // If there is no hard keyboard or there is a hard keyboard currently hidden,
                        // CTRL-1 through CTRL-9 will send F1 through F9
                        if ((!hardKeyboard || hardKeyboardHidden) && sendFunctionKey(keyCode))
                            return true;

                        uchar = keyAsControl(uchar);
                    }

                    // handle pressing f-keys
                    if ((hardKeyboard && !hardKeyboardHidden)
                            && (curMetaState & KeyEvent.META_ALT_ON) != 0
                            && (curMetaState & KeyEvent.META_SHIFT_ON) != 0
                            && sendFunctionKey(keyCode))
                        return true;

                    if (uchar < 0x80)
                        buffer.write(uchar);
                    else
                        buffer.write(new String(Character.toChars(uchar)).getBytes(encoding));

                    return true;
                }

                // send ctrl and meta-keys as appropriate
                if (!hardKeyboard || hardKeyboardHidden) {
                    int k = event.getUnicodeChar(0);
                    int k0 = k;
                    boolean sendCtrl = false;
                    boolean sendMeta = false;

                    if (k != 0) {
                        if ((orgMetaState & HC_META_CTRL_ON) != 0) {
                            k = keyAsControl(k);

                            if (k != k0)
                                sendCtrl = true;

                            // send F1-F10 via CTRL-1 through CTRL-0
                            if (!sendCtrl && sendFunctionKey(keyCode))
                                return true;
                        }
                        else if ((orgMetaState & KeyEvent.META_ALT_ON) != 0) {
                            sendMeta = true;
                            buffer.write(0x1b);
                        }

                        if (sendMeta || sendCtrl) {
                            buffer.write(k);
                            return true;
                        }
                    }
                }

                // handle meta and f-keys for full hardware keyboard
                if (hardKeyboard && !hardKeyboardHidden && fullKeyboard()) {
                    int k = event.getUnicodeChar(orgMetaState & KeyEvent.META_SHIFT_ON);
                    int k0 = k;

                    if (k != 0) {
                        if ((orgMetaState & HC_META_CTRL_ON) != 0) {
                            k = keyAsControl(k);

                            if (k != k0)
                                buffer.write(k);

                            return true;
                        }
                        else if ((orgMetaState & KeyEvent.META_ALT_ON) != 0) {
                            buffer.write(0x1b);
                            buffer.write(k);
                            return true;
                        }
                    }

                    if (sendFullSpecialKey(keyCode))
                        return true;
                }

                // try handling keymode shortcuts
                if (hardKeyboard && !hardKeyboardHidden &&
                        event.getRepeatCount() == 0) {
                    if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
                        switch (keyCode) {
                            case KeyEvent.KEYCODE_ALT_RIGHT:
                                metaState |= META_SLASH;
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_RIGHT:
                                metaState |= META_TAB;
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_LEFT:
                                metaPress(META_SHIFT_ON);
                                return true;

                            case KeyEvent.KEYCODE_ALT_LEFT:
                                metaPress(META_ALT_ON);
                                return true;
                        }
                    }
                    else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
                        switch (keyCode) {
                            case KeyEvent.KEYCODE_ALT_LEFT:
                                metaState |= META_SLASH;
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_LEFT:
                                metaState |= META_TAB;
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_RIGHT:
                                metaPress(META_SHIFT_ON);
                                return true;

                            case KeyEvent.KEYCODE_ALT_RIGHT:
                                metaPress(META_ALT_ON);
                                return true;
                        }
                    }
                    else {
                        switch (keyCode) {
                            case KeyEvent.KEYCODE_ALT_RIGHT:
                            case KeyEvent.KEYCODE_ALT_LEFT:
                                metaPress(META_ALT_ON);
                                return true;

                            case KeyEvent.KEYCODE_SHIFT_LEFT:
                            case KeyEvent.KEYCODE_SHIFT_RIGHT:
                                metaPress(META_SHIFT_ON);
                                return true;
                        }
                    }

                    // Handle hardware CTRL keys
                    if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
                            keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
                        ctrlKeySpecial();
                        return true;
                    }
                }

                // look for special chars
                switch (keyCode) {
                    case KEYCODE_ESCAPE:
                        buffer.write(0x1b);
                        return true;

                    case KeyEvent.KEYCODE_TAB:
                        buffer.write("[tab]");
                        return true;

                    case KEYCODE_PAGE_DOWN:
                        buffer.write("[pgdown]");
                        metaState &= ~META_TRANSIENT;
                        bridge.tryKeyVibrate();
                        return true;

                    case KEYCODE_PAGE_UP:
                        buffer.write("[pgup]");
                        metaState &= ~META_TRANSIENT;
                        bridge.tryKeyVibrate();
                        return true;

                    case KeyEvent.KEYCODE_MOVE_HOME:
                        buffer.write("[home]");
                        metaState &= ~META_TRANSIENT;
                        bridge.tryKeyVibrate();
                        return true;

                    case KeyEvent.KEYCODE_MOVE_END:
                        buffer.write("[end]");  // does not exist!!
                        metaState &= ~META_TRANSIENT;
                        bridge.tryKeyVibrate();
                        return true;

                    case KeyEvent.KEYCODE_CAMERA:
                        // check to see which shortcut the camera button triggers
                        String hwbuttonShortcut = manager.prefs.getString(
                                                      PreferenceConstants.CAMERA,
                                                      PreferenceConstants.HWBUTTON_SCREEN_CAPTURE);
                        return (handleShortcut(v, hwbuttonShortcut));

                    case KeyEvent.KEYCODE_VOLUME_UP:
                        // check to see which shortcut the volume button triggers
                        hwbuttonShortcut = manager.prefs.getString(
                                               PreferenceConstants.VOLUP,
                                               PreferenceConstants.HWBUTTON_CTRL);
                        return (handleShortcut(v, hwbuttonShortcut));

                    case KeyEvent.KEYCODE_VOLUME_DOWN:
                        // check to see which shortcut the camera button triggers
                        hwbuttonShortcut = manager.prefs.getString(
                                               PreferenceConstants.VOLDN,
                                               PreferenceConstants.HWBUTTON_TAB);
                        return (handleShortcut(v, hwbuttonShortcut));

                    case KeyEvent.KEYCODE_SEARCH:
                        // check to see which shortcut the search button triggers
                        hwbuttonShortcut = manager.prefs.getString(
                                               PreferenceConstants.SEARCH,
                                               PreferenceConstants.HWBUTTON_ESC);
                        return (handleShortcut(v, hwbuttonShortcut));

                    case KeyEvent.KEYCODE_DEL:
                        if ((metaState & META_ALT_MASK) != 0) {
                            buffer.write("[insert]");
                        }
                        else {
                            buffer.write("[backspace]");
                        }

                        metaState &= ~META_TRANSIENT;
                        return true;

                    case KeyEvent.KEYCODE_ENTER:
                        buffer.write("[enter]");
                        metaState &= ~META_TRANSIENT;
                        return true;

                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        if (selectingForCopy) {
                            selectionArea.decrementColumn();
                            bridge.redraw();
                        }
                        else {
                            if ((metaState & META_ALT_MASK) != 0) {
                                buffer.write("[home]");
                            }
                            else {
                                buffer.write("[left]");
                            }

                            metaState &= ~META_TRANSIENT;
                            bridge.tryKeyVibrate();
                        }

                        return true;

                    case KeyEvent.KEYCODE_DPAD_UP:
                        if (selectingForCopy) {
                            selectionArea.decrementRow();
                            bridge.redraw();
                        }
                        else {
                            if ((metaState & META_ALT_MASK) != 0) {
                                buffer.write("[pgup]");
                            }
                            else {
                                buffer.write("[up]");
                            }

                            metaState &= ~META_TRANSIENT;
                            bridge.tryKeyVibrate();
                        }

                        return true;

                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        if (selectingForCopy) {
                            selectionArea.incrementRow();
                            bridge.redraw();
                        }
                        else {
                            if ((metaState & META_ALT_MASK) != 0) {
                                buffer.write("[pgdown]");
                            }
                            else {
                                buffer.write("[down]");
                            }

                            metaState &= ~META_TRANSIENT;
                            bridge.tryKeyVibrate();
                        }

                        return true;

                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        if (selectingForCopy) {
                            selectionArea.incrementColumn();
                            bridge.redraw();
                        }
                        else {
                            if ((metaState & META_ALT_MASK) != 0) {
                                buffer.write("[end]");
                            }
                            else {
                                buffer.write("[right]");
                            }

                            metaState &= ~META_TRANSIENT;
                            bridge.tryKeyVibrate();
                        }

                        return true;

                    case KeyEvent.KEYCODE_DPAD_CENTER:
                        ctrlKeySpecial();
                        return true;
                }
            }
            catch (IOException e) {
                Log.e(TAG, "Problem while trying to handle an onKey() event", e);

                try {
                    bridge.transport.flush();
                }
                catch (IOException ioe) {
                    Log.d(TAG, "Our transport was closed, dispatching disconnect event");
                    bridge.dispatchDisconnect(false);
                }
            }
            catch (NullPointerException npe) {
                Log.d(TAG, "Input before connection established ignored.");
                return true;
            }
        }

    };


    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);
        handler.setSSLType("TLS");
        screen52.setVT(handler);
        screen52.setBuffer(buffer);
        connected = handler.connect(host.getHostname(), host.getPort());
        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.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) {
            host.setNickname(getDefaultNickname(host.getUsername(),
                                                host.getHostname(), host.getPort()));
        }
        else {
            host.setNickname(uri.getFragment());
        }

        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 Terminal5250KeyListener(manager, bridge, buffer, host.getEncoding());
    }

}