view src/com/five_ten_sg/connectbot/service/TerminalBridge.java @ 249:dc6e53600ae2

add host preference for fixed screen size
author Carl Byington <carl@five-ten-sg.com>
date Mon, 14 Jul 2014 11:51:59 -0700
parents 0181323d0d33
children e13878bb3ddf
line wrap: on
line source

/*
 * ConnectBot: simple, powerful, open-source SSH client for Android
 * Copyright 2007 Kenny Root, Jeffrey Sharkey
 *
 * 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.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.five_ten_sg.connectbot.R;
import com.five_ten_sg.connectbot.TerminalView;
import com.five_ten_sg.connectbot.bean.HostBean;
import com.five_ten_sg.connectbot.bean.PortForwardBean;
import com.five_ten_sg.connectbot.bean.SelectionArea;
import com.five_ten_sg.connectbot.transport.AbsTransport;
import com.five_ten_sg.connectbot.transport.TransportFactory;
import com.five_ten_sg.connectbot.util.HostDatabase;
import com.five_ten_sg.connectbot.util.PreferenceConstants;
import com.five_ten_sg.connectbot.util.StringPickerDialog;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Typeface;
import android.os.Binder;
import android.os.Environment;
import android.text.ClipboardManager;
import android.text.Editable;
import android.text.method.CharacterPickerDialog;
import android.util.FloatMath;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
import de.mud.terminal.vt320;


/**
 * Provides a bridge between a MUD terminal buffer and a possible TerminalView.
 * This separation allows us to keep the TerminalBridge running in a background
 * service. A TerminalView shares down a bitmap that we can use for rendering
 * when available.
 *
 * This class also provides SSH hostkey verification prompting, and password
 * prompting.
 */
@SuppressWarnings("deprecation")  // for ClipboardManager
public class TerminalBridge implements VDUDisplay {
    public final static String TAG = "ConnectBot.TerminalBridge";

    private final static float  FONT_SIZE_FACTOR  = 1.1f;

    public Integer[] color;

    public int defaultFg = HostDatabase.DEFAULT_FG_COLOR;
    public int defaultBg = HostDatabase.DEFAULT_BG_COLOR;

    protected final TerminalManager manager;
    public    final HostBean        host;
    public    final String          homeDirectory;

    AbsTransport transport;

    final Paint defaultPaint;

    private Relay relay;

    private String emulation;   // aka answerback string, aka terminal type

    public Bitmap bitmap = null;
    public vt320  buffer = null;

    public  TerminalView parent = null;
    private final Canvas canvas = new Canvas();

    private boolean disconnected = false;
    private boolean awaitingClose = false;

    private boolean forcedSize = false;
    private int columns;
    private int rows;

    public  TerminalMonitor     monitor = null;
    private TerminalKeyListener keyListener = null;

    private boolean selectingForCopy = false;
    private final SelectionArea selectionArea;

    // TODO add support for the new clipboard API
    private ClipboardManager clipboard;

    public  int   charWidth  = -1;
    public  int   charHeight = -1;
    private int   charTop    = -1;
    private float fontSize   = -1;

    private final List<FontSizeChangedListener> fontSizeChangedListeners;

    private final List<String> localOutput;

    /**
     * Flag indicating if we should perform a full-screen redraw during our next
     * rendering pass.
     */
    private boolean fullRedraw = false;

    public PromptHelper promptHelper;

    protected BridgeDisconnectedListener disconnectListener = null;

    /**
     * Create a new terminal bridge suitable for unit testing.
     */
    public TerminalBridge() {
        buffer = new vt320() {
            @Override
            public void write(byte[] b) {}
            @Override
            public void write(int b) {}
            @Override
            public void sendTelnetCommand(byte cmd) {}
            @Override
            public void setWindowSize(int c, int r) {}
            @Override
            public void debug(String s) {}
        };
        emulation     = null;
        manager       = null;
        host          = null;
        homeDirectory = null;
        defaultPaint  = new Paint();
        selectionArea = new SelectionArea();
        localOutput   = new LinkedList<String>();
        fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
        transport   = null;
        keyListener = new TerminalKeyListener(manager, this, buffer, null);
        monitor     = null;
    }

    /**
     * Create new terminal bridge with following parameters.
     */
    public TerminalBridge(final TerminalManager manager, final HostBean host, final String homeDirectory) throws IOException {
        this.manager       = manager;
        this.host          = host;
        this.homeDirectory = homeDirectory;
        emulation = host.getHostEmulation();

        if ((emulation == null) || (emulation.length() == 0)) emulation = manager.getEmulation();

        // create prompt helper to relay password and hostkey requests up to gui
        promptHelper = new PromptHelper(this);
        // create our default paint
        defaultPaint = new Paint();
        defaultPaint.setAntiAlias(true);
        defaultPaint.setTypeface(Typeface.MONOSPACE);
        defaultPaint.setFakeBoldText(true);  // more readable?
        localOutput = new LinkedList<String>();
        fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
        setMyFontSize();
        resetColors();
        selectionArea = new SelectionArea();
    }

    public PromptHelper getPromptHelper() {
        return promptHelper;
    }

    /**
     * Spawn thread to open connection and start login process.
     */
    protected void startConnection() {
        transport   = TransportFactory.getTransport(host.getProtocol());
        transport.setLinks(manager, this, homeDirectory, host, emulation);
        buffer      = transport.getTransportBuffer();
        keyListener = transport.getTerminalKeyListener();
        String monitor_init = host.getMonitor();

        if ((monitor_init != null) && (monitor_init.length() > 0)) {
            monitor = new TerminalMonitor(manager, buffer, parent, host, monitor_init);
        }

        transport.setCompression(host.getCompression());
        transport.setHttpproxy(host.getHttpproxy());
        transport.setUseAuthAgent(host.getUseAuthAgent());

        if (transport.canForwardPorts()) {
            for (PortForwardBean portForward : manager.hostdb.getPortForwardsForHost(host))
                transport.addPortForward(portForward);
        }

        outputLine(manager.res.getString(R.string.terminal_connecting, host.getHostname(), host.getPort(), host.getProtocol()));
        Thread connectionThread = new Thread(new Runnable() {
            public void run() {
                transport.connect();
            }
        });
        connectionThread.setName("Connection");
        connectionThread.setDaemon(true);
        connectionThread.start();
    }

    /**
     * Handle challenges from keyboard-interactive authentication mode.
     */
    public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
        String[] responses = new String[numPrompts];

        for (int i = 0; i < numPrompts; i++) {
            // request response from user for each prompt
            responses[i] = promptHelper.requestPasswordPrompt(instruction, prompt[i]);
        }

        return responses;
    }

    /**
     * @return charset in use by bridge
     */
    public Charset getCharset() {
        if (relay != null) return relay.getCharset();

        return keyListener.getCharset();
    }

    /**
     * Sets the encoding used by the terminal. If the connection is live,
     * then the character set is changed for the next read.
     * @param encoding the canonical name of the character encoding
     */
    public void setCharset(String encoding) {
        if (relay != null) relay.setCharset(encoding);

        keyListener.setCharset(encoding);
    }

    /**
     * Convenience method for writing a line into the underlying MUD buffer.
     * Should never be called once the session is established.
     */
    public final void outputLine(String line) {
        if (transport != null && transport.isSessionOpen())
            Log.e(TAG, "Session established, cannot use outputLine!", new IOException("outputLine call traceback"));

        synchronized (localOutput) {
            final String s = line + "\r\n";
            localOutput.add(s);
            buffer.putString(s);
            // For accessibility
            final char[] charArray = s.toCharArray();
            propagateConsoleText(charArray, charArray.length);
        }
    }

    /**
     * Inject a specific string into this terminal. Used for post-login strings
     * and pasting clipboard.
     */
    public void injectString(final String string) {
        if (string == null || string.length() == 0)
            return;

        Thread injectStringThread = new Thread(new Runnable() {
            public void run() {
                try {
                    transport.write(string.getBytes(host.getEncoding()));
                }
                catch (Exception e) {
                    Log.e(TAG, "Couldn't inject string to remote host: ", e);
                }
            }
        });
        injectStringThread.setName("InjectString");
        injectStringThread.start();
    }

    /**
     * Internal method to request actual PTY terminal once we've finished
     * authentication. If called before authenticated, it will just fail.
     */
    public void onConnected() {
        disconnected = false;
        buffer.reset();
        buffer.setAnswerBack(emulation);
        localOutput.clear();    // We no longer need our local output.

        if (HostDatabase.DELKEY_BACKSPACE.equals(host.getDelKey()))
            buffer.setBackspace(vt320.DELETE_IS_BACKSPACE);
        else
            buffer.setBackspace(vt320.DELETE_IS_DEL);

        // create thread to relay incoming connection data to buffer
        // only if needed by the transport
        if (transport.needsRelay()) {
            relay = new Relay(this, transport, buffer, host.getEncoding());
            Thread relayThread = new Thread(relay);
            relayThread.setDaemon(true);
            relayThread.setName("Relay");
            relayThread.start();
        }

        // get proper font size
        setMyFontSize();

        // finally send any post-login string, if requested
        injectString(host.getPostLogin());
    }

    private void setMyFontSize() {
        if ((parent != null) && (host.getFixedSize())) {
            resizeComputed(host.getFixedWidth(), host.getFixedHeight(), parent.getWidth(), parent.getHeight());
        }
        else {
            setFontSize(host.getFontSize());
        }
    }

    /**
     * @return whether a session is open or not
     */
    public boolean isSessionOpen() {
        if (transport != null) return transport.isSessionOpen();

        return false;
    }

    public void setOnDisconnectedListener(BridgeDisconnectedListener disconnectListener) {
        this.disconnectListener = disconnectListener;
    }

    /**
     * Force disconnection of this terminal bridge.
     */
    public void dispatchDisconnect(boolean immediate) {
        // We don't need to do this multiple times.
        synchronized (this) {
            if (disconnected && !immediate) return;

            disconnected = true;
        }

        // Cancel any pending prompts.
        promptHelper.cancelPrompt();
        // disconnection request hangs if we havent really connected to a host yet
        // temporary fix is to just spawn disconnection into a thread
        Thread disconnectThread = new Thread(new Runnable() {
            public void run() {
                if (transport != null && transport.isConnected())
                    transport.close();
            }
        });
        disconnectThread.setName("Disconnect");
        disconnectThread.start();

        if (immediate) {
            awaitingClose = true;

            if (disconnectListener != null)
                disconnectListener.onDisconnected(TerminalBridge.this);
        }
        else {
            final String line = manager.res.getString(R.string.alert_disconnect_msg);
            buffer.putString("\r\n" + line + "\r\n");

            if (host.getStayConnected()) {
                manager.requestReconnect(this);
                return;
            }

            Thread disconnectPromptThread = new Thread(new Runnable() {
                public void run() {
                    Boolean result = promptHelper.requestBooleanPrompt(null,
                                     manager.res.getString(R.string.prompt_host_disconnected));

                    if (result == null || result.booleanValue()) {
                        awaitingClose = true;

                        // Tell the TerminalManager that we can be destroyed now.
                        if (disconnectListener != null)
                            disconnectListener.onDisconnected(TerminalBridge.this);
                    }
                }
            });
            disconnectPromptThread.setName("DisconnectPrompt");
            disconnectPromptThread.setDaemon(true);
            disconnectPromptThread.start();
        }

        // close the monitor
        if (monitor != null) monitor.Disconnect();

        monitor = null;
    }

    public void setSelectingForCopy(boolean selectingForCopy) {
        this.selectingForCopy = selectingForCopy;
    }

    public boolean isSelectingForCopy() {
        return selectingForCopy;
    }

    public SelectionArea getSelectionArea() {
        return selectionArea;
    }

    public synchronized void tryKeyVibrate() {
        manager.tryKeyVibrate();
    }

    /**
     * Request a different font size. Will make call to parentChanged() to make
     * sure we resize PTY if needed.
     */
    final void setFontSize(float size) {
        if (size <= 0.0) size = 12.0f;
        size = (float)(int)((size * 10.0f) + 0.5f) / 10.0f;
        defaultPaint.setTextSize(size);
        fontSize = size;
        // read new metrics to get exact pixel dimensions
        FontMetrics fm = defaultPaint.getFontMetrics();
        charTop = (int)FloatMath.ceil(fm.top);
        float[] widths = new float[1];
        defaultPaint.getTextWidths("X", widths);
        charWidth = (int)FloatMath.ceil(widths[0]);
        charHeight = (int)FloatMath.ceil(fm.descent - fm.top);

        // refresh any bitmap with new font size
        if (parent != null) parentChanged(parent);

        for (FontSizeChangedListener ofscl : fontSizeChangedListeners)
            ofscl.onFontSizeChanged(size);

        host.setFontSize(size);
        manager.hostdb.updateFontSize(host);
    }

    /**
     * Add an {@link FontSizeChangedListener} to the list of listeners for this
     * bridge.
     *
     * @param listener
     *            listener to add
     */
    public void addFontSizeChangedListener(FontSizeChangedListener listener) {
        fontSizeChangedListeners.add(listener);
    }

    /**
     * Remove an {@link FontSizeChangedListener} from the list of listeners for
     * this bridge.
     *
     * @param listener
     */
    public void removeFontSizeChangedListener(FontSizeChangedListener listener) {
        fontSizeChangedListeners.remove(listener);
    }

    /**
     * Something changed in our parent {@link TerminalView}, maybe it's a new
     * parent, or maybe it's an updated font size. We should recalculate
     * terminal size information and request a PTY resize.
     */

    public final synchronized void parentChanged(TerminalView parent) {
        if (manager != null && !manager.isResizeAllowed()) {
            Log.d(TAG, "Resize is not allowed now");
            return;
        }

        this.parent = parent;
        final int width = parent.getWidth();
        final int height = parent.getHeight();

        // Something has gone wrong with our layout; we're 0 width or height!
        if (width <= 0 || height <= 0)
            return;

        clipboard = (ClipboardManager) parent.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
        keyListener.setClipboardManager(clipboard);

        if (!forcedSize) {
            // recalculate buffer size
            int newColumns, newRows;
            newColumns = width / charWidth;
            newRows = height / charHeight;

            // If nothing has changed in the terminal dimensions and not an intial
            // draw then don't blow away scroll regions and such.
            if (newColumns == columns && newRows == rows)
                return;

            columns = newColumns;
            rows = newRows;
        }

        // reallocate new bitmap if needed
        boolean newBitmap = (bitmap == null);

        if (bitmap != null)
            newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height);

        if (newBitmap) {
            discardBitmap();
            bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
            canvas.setBitmap(bitmap);
        }

        // clear out any old buffer information
        defaultPaint.setColor(Color.BLACK);
        canvas.drawPaint(defaultPaint);

        // Stroke the border of the terminal if the size is being forced;
        if (forcedSize) {
            int borderX = (columns * charWidth) + 1;
            int borderY = (rows * charHeight) + 1;
            defaultPaint.setColor(Color.GRAY);
            defaultPaint.setStrokeWidth(0.0f);

            if (width >= borderX)
                canvas.drawLine(borderX, 0, borderX, borderY + 1, defaultPaint);

            if (height >= borderY)
                canvas.drawLine(0, borderY, borderX + 1, borderY, defaultPaint);
        }

        try {
            // request a terminal pty resize
            if (buffer != null) {
                synchronized (buffer) {
                    buffer.setScreenSize(columns, rows, true);
                }
            }

            if (transport != null)
                transport.setDimensions(columns, rows, width, height);
        }
        catch (Exception e) {
            Log.e(TAG, "Problem while trying to resize screen or PTY", e);
        }

        // redraw local output if we don't have a session to receive our resize request
        if (transport == null) {
            synchronized (localOutput) {
                buffer.reset();

                for (String line : localOutput)
                    buffer.putString(line);
            }
        }

        // force full redraw with new buffer size
        fullRedraw = true;
        redraw();
        parent.notifyUser(String.format("%d x %d", columns, rows));
        Log.i(TAG, String.format("parentChanged() now width=%d, height=%d", columns, rows));
    }

    /**
     * Somehow our parent {@link TerminalView} was destroyed. Now we don't need
     * to redraw anywhere, and we can recycle our internal bitmap.
     */

    public synchronized void parentDestroyed() {
        parent = null;
        discardBitmap();
    }

    private void discardBitmap() {
        if (bitmap != null)
            bitmap.recycle();

        bitmap = null;
    }

    public void propagateConsoleText(char[] rawText, int length) {
        if (parent != null) {
            parent.propagateConsoleText(rawText, length);
        }
    }

    public void onDraw() {
        int fg, bg;

        synchronized (buffer) {
            boolean entireDirty = buffer.update[0] || fullRedraw;
            boolean isWideCharacter = false;

            // walk through all lines in the buffer
            for (int l = 0; l < buffer.height; l++) {
                // check if this line is dirty and needs to be repainted
                // also check for entire-buffer dirty flags
                if (!entireDirty && !buffer.update[l + 1]) continue;

                // reset dirty flag for this line
                buffer.update[l + 1] = false;

                // walk through all characters in this line
                for (int c = 0; c < buffer.width; c++) {
                    int addr = 0;
                    int currAttr = buffer.charAttributes[buffer.windowBase + l][c];
                    {
                        int fgcolor = defaultFg;

                        // check if foreground color attribute is set
                        if ((currAttr & VDUBuffer.COLOR_FG) != 0)
                            fgcolor = ((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1;

                        if (fgcolor < 8 && (currAttr & VDUBuffer.BOLD) != 0)
                            fg = color[fgcolor + 8];
                        else
                            fg = color[fgcolor];
                    }

                    // check if background color attribute is set
                    if ((currAttr & VDUBuffer.COLOR_BG) != 0)
                        bg = color[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1];
                    else
                        bg = color[defaultBg];

                    // support character inversion by swapping background and foreground color
                    if ((currAttr & VDUBuffer.INVERT) != 0) {
                        int swapc = bg;
                        bg = fg;
                        fg = swapc;
                    }

                    // set underlined attributes if requested
                    defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0);
                    isWideCharacter = (currAttr & VDUBuffer.FULLWIDTH) != 0;

                    if (isWideCharacter)
                        addr++;
                    else {
                        // determine the amount of continuous characters with the same settings and print them all at once
                        while (c + addr < buffer.width
                                && buffer.charAttributes[buffer.windowBase + l][c + addr] == currAttr) {
                            addr++;
                        }
                    }

                    // Save the current clip region
                    canvas.save(Canvas.CLIP_SAVE_FLAG);
                    // clear this dirty area with background color
                    defaultPaint.setColor(bg);

                    if (isWideCharacter) {
                        canvas.clipRect(c * charWidth,
                                        l * charHeight,
                                        (c + 2) * charWidth,
                                        (l + 1) * charHeight);
                    }
                    else {
                        canvas.clipRect(c * charWidth,
                                        l * charHeight,
                                        (c + addr) * charWidth,
                                        (l + 1) * charHeight);
                    }

                    canvas.drawPaint(defaultPaint);
                    // write the text string starting at 'c' for 'addr' number of characters
                    defaultPaint.setColor(fg);

                    if ((currAttr & VDUBuffer.INVISIBLE) == 0)
                        canvas.drawText(buffer.charArray[buffer.windowBase + l], c,
                                        addr, c * charWidth, (l * charHeight) - charTop,
                                        defaultPaint);

                    // Restore the previous clip region
                    canvas.restore();
                    // advance to the next text block with different characteristics
                    c += addr - 1;

                    if (isWideCharacter)
                        c++;
                }
            }

            // reset entire-buffer flags
            buffer.update[0] = false;
        }

        fullRedraw = false;
    }

    public void redraw() {
        if (parent != null)
            parent.postInvalidate();
    }

    // We don't have a scroll bar.
    public void updateScrollBar() {
    }

    /**
     * Resize terminal to fit [rows]x[cols] in screen of size [width]x[height]
     * @param rows
     * @param cols
     * @param width
     * @param height
     */

    public synchronized void resizeComputed(int cols, int rows, int width, int height) {
        float size = 8.0f;
        float step = 8.0f;
        float limit = 0.125f;
        int direction;

        while ((direction = fontSizeCompare(size, cols, rows, width, height)) < 0)
            size += step;

        if (direction == 0) {
            Log.d("fontsize", String.format("Found match at %f", size));
            return;
        }

        step /= 2.0f;
        size -= step;

        while ((direction = fontSizeCompare(size, cols, rows, width, height)) != 0
                && step >= limit) {
            step /= 2.0f;

            if (direction > 0) {
                size -= step;
            }
            else {
                size += step;
            }
        }

        if (direction > 0)
            size -= step;

        this.columns = cols;
        this.rows = rows;
        forcedSize = true;
        setFontSize(size);
    }

    private int fontSizeCompare(float size, int cols, int rows, int width, int height) {
        // read new metrics to get exact pixel dimensions
        defaultPaint.setTextSize(size);
        FontMetrics fm = defaultPaint.getFontMetrics();
        float[] widths = new float[1];
        defaultPaint.getTextWidths("X", widths);
        int termWidth = (int)widths[0] * cols;
        int termHeight = (int)FloatMath.ceil(fm.descent - fm.top) * rows;
        Log.d("fontsize", String.format("font size %f resulted in %d x %d", size, termWidth, termHeight));

        // Check to see if it fits in resolution specified.
        if (termWidth > width || termHeight > height)
            return 1;

        if (termWidth == width || termHeight == height)
            return 0;

        return -1;
    }

    /**
     * @return whether underlying transport can forward ports
     */
    public boolean canFowardPorts() {
        return transport.canForwardPorts();
    }

    /**
     * Adds the {@link PortForwardBean} to the list.
     * @param portForward the port forward bean to add
     * @return true on successful addition
     */
    public boolean addPortForward(PortForwardBean portForward) {
        return transport.addPortForward(portForward);
    }

    /**
     * Removes the {@link PortForwardBean} from the list.
     * @param portForward the port forward bean to remove
     * @return true on successful removal
     */
    public boolean removePortForward(PortForwardBean portForward) {
        return transport.removePortForward(portForward);
    }

    /**
     * @return the list of port forwards
     */
    public List<PortForwardBean> getPortForwards() {
        return transport.getPortForwards();
    }

    /**
     * Enables a port forward member. After calling this method, the port forward should
     * be operational.
     * @param portForward member of our current port forwards list to enable
     * @return true on successful port forward setup
     */
    public boolean enablePortForward(PortForwardBean portForward) {
        if (!transport.isConnected()) {
            Log.i(TAG, "Attempt to enable port forward while not connected");
            return false;
        }

        return transport.enablePortForward(portForward);
    }

    /**
     * Disables a port forward member. After calling this method, the port forward should
     * be non-functioning.
     * @param portForward member of our current port forwards list to enable
     * @return true on successful port forward tear-down
     */
    public boolean disablePortForward(PortForwardBean portForward) {
        if (!transport.isConnected()) {
            Log.i(TAG, "Attempt to disable port forward while not connected");
            return false;
        }

        return transport.disablePortForward(portForward);
    }

    /**
     * @return whether underlying transport can transfer files
     */
    public boolean canTransferFiles() {
        return transport.canTransferFiles();
    }

    /**
     * Downloads the specified remote file to the local connectbot folder.
     * @return true on success, false on failure
     */
    public boolean downloadFile(String remoteFile, String localFolder) {
        return transport.downloadFile(remoteFile, localFolder);
    }

    /**
     * Uploads the specified local file to the remote host's default directory.
     * @return true on success, false on failure
     */
    public boolean uploadFile(String localFile, String remoteFolder, String remoteFile, String mode) {
        if (mode == null)
            mode = "0600";

        return transport.uploadFile(localFile, remoteFolder, remoteFile, mode);
    }

    /**
     * @return whether the TerminalBridge should close
     */
    public boolean isAwaitingClose() {
        return awaitingClose;
    }

    /**
     * @return whether this connection had started and subsequently disconnected
     */
    public boolean isDisconnected() {
        return disconnected;
    }

    /* (non-Javadoc)
     * @see de.mud.terminal.VDUDisplay#setColor(byte, byte, byte, byte)
     */
    public void setColor(int index, int red, int green, int blue) {
        // Don't allow the system colors to be overwritten for now. May violate specs.
        if (index < color.length && index >= 16)
            color[index] = 0xff000000 | red << 16 | green << 8 | blue;
    }

    public final void resetColors() {
        int[] defaults = manager.hostdb.getDefaultColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME);
        defaultFg = defaults[0];
        defaultBg = defaults[1];
        color = manager.hostdb.getColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME);
    }

    private static Pattern urlPattern = null;

    /**
     * @return
     */
    public List<String> scanForURLs() {
        Set<String> urls = new LinkedHashSet<String>();

        if (urlPattern == null) {
            // based on http://www.ietf.org/rfc/rfc2396.txt
            String scheme = "[A-Za-z][-+.0-9A-Za-z]*";
            String unreserved = "[-._~0-9A-Za-z]";
            String pctEncoded = "%[0-9A-Fa-f]{2}";
            String subDelims = "[!$&'()*+,;:=]";
            String userinfo = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|:)*";
            String h16 = "[0-9A-Fa-f]{1,4}";
            String decOctet = "(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])";
            String ipv4address = decOctet + "\\." + decOctet + "\\." + decOctet + "\\." + decOctet;
            String ls32 = "(?:" + h16 + ":" + h16 + "|" + ipv4address + ")";
            String ipv6address = "(?:(?:" + h16 + "){6}" + ls32 + ")";
            String ipvfuture = "v[0-9A-Fa-f]+.(?:" + unreserved + "|" + subDelims + "|:)+";
            String ipLiteral = "\\[(?:" + ipv6address + "|" + ipvfuture + ")\\]";
            String regName = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + ")*";
            String host = "(?:" + ipLiteral + "|" + ipv4address + "|" + regName + ")";
            String port = "[0-9]*";
            String authority = "(?:" + userinfo + "@)?" + host + "(?::" + port + ")?";
            String pchar = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|@)";
            String segment = pchar + "*";
            String pathAbempty = "(?:/" + segment + ")*";
            String segmentNz = pchar + "+";
            String pathAbsolute = "/(?:" + segmentNz + "(?:/" + segment + ")*)?";
            String pathRootless = segmentNz + "(?:/" + segment + ")*";
            String hierPart = "(?://" + authority + pathAbempty + "|" + pathAbsolute + "|" + pathRootless + ")";
            String query = "(?:" + pchar + "|/|\\?)*";
            String fragment = "(?:" + pchar + "|/|\\?)*";
            String uriRegex = scheme + ":" + hierPart + "(?:" + query + ")?(?:#" + fragment + ")?";
            urlPattern = Pattern.compile(uriRegex);
        }

        char[] visibleBuffer = new char[buffer.height * buffer.width];

        for (int l = 0; l < buffer.height; l++)
            System.arraycopy(buffer.charArray[buffer.windowBase + l], 0,
                             visibleBuffer, l * buffer.width, buffer.width);

        Matcher urlMatcher = urlPattern.matcher(new String(visibleBuffer));

        while (urlMatcher.find())
            urls.add(urlMatcher.group());

        return (new LinkedList<String> (urls));
    }

    /**
     * @return
     */
    public boolean isUsingNetwork() {
        return transport.usesNetwork();
    }

    /**
     * @return
     */
    public TerminalKeyListener getKeyHandler() {
        return keyListener;
    }

    /**
     *
     */
    public void resetScrollPosition() {
        // if we're in scrollback, scroll to bottom of window on input
        if (buffer.windowBase != buffer.screenBase)
            buffer.setWindowBase(buffer.screenBase);
    }

    /**
     *
     */
    public void increaseFontSize() {
        setFontSize(fontSize * FONT_SIZE_FACTOR);
    }

    /**
     *
     */
    public void decreaseFontSize() {
        setFontSize(fontSize / FONT_SIZE_FACTOR);
    }

    /**
     * Auto-size window back to default
     */
    public void resetSize(TerminalView parent) {
        this.forcedSize = false;
        setMyFontSize();
    }

    /**
     * Create a screenshot of the current view
     */
    public void captureScreen() {
        String msg;
        File dir, path;
        boolean success = true;
        Bitmap screenshot = this.bitmap;

        if (manager == null || parent == null || screenshot == null)
            return;

        SimpleDateFormat s = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String date = s.format(new Date());
        String pref_path = manager.prefs.getString(PreferenceConstants.SCREEN_CAPTURE_FOLDER, "");
        File default_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

        if (pref_path.equals(""))
            dir = default_path;
        else
            dir = new File(pref_path);

        path = new File(dir, "vx-" + date + ".png");

        try {
            dir.mkdirs();
            FileOutputStream out = new FileOutputStream(path);
            screenshot.compress(Bitmap.CompressFormat.PNG, 90, out);
            out.close();
        }
        catch (Exception e) {
            e.printStackTrace();
            success = false;
        }

        if (success) {
            msg = manager.getResources().getString(R.string.screenshot_saved_as) + " " + path;

            if (manager.prefs.getBoolean(PreferenceConstants.SCREEN_CAPTURE_POPUP, true)) {
                new AlertDialog.Builder(parent.getContext())
                .setTitle(R.string.screenshot_success_title)
                .setMessage(msg)
                .setPositiveButton(R.string.button_close, null)
                .show();
            }
        }
        else {
            msg = manager.getResources().getString(R.string.screenshot_not_saved_as) + " " + path;
            new AlertDialog.Builder(parent.getContext())
            .setTitle(R.string.screenshot_error_title)
            .setMessage(msg)
            .setNegativeButton(R.string.button_close, null)
            .show();
        }

        return;
    }

    /**
     * Show change font size dialog
     */
    public boolean showFontSizeDialog() {
        final String pickerString = "+-";
        CharSequence str = "";
        Editable content = Editable.Factory.getInstance().newEditable(str);

        if (parent == null)
            return false;

        CharacterPickerDialog cpd = new CharacterPickerDialog(parent.getContext(),
        parent, content, pickerString, true) {
            private void changeFontSize(CharSequence result) {
                if (result.equals("+"))
                    increaseFontSize();
                else if (result.equals("-"))
                    decreaseFontSize();
            }
            @Override
            public void onItemClick(AdapterView p, View v, int pos, long id) {
                final String result = String.valueOf(pickerString.charAt(pos));
                changeFontSize(result);
            }
            @Override
            public void onClick(View v) {
                if (v instanceof Button) {
                    final CharSequence result = ((Button) v).getText();

                    if (result.equals(""))
                        dismiss();
                    else
                        changeFontSize(result);
                }
            }
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
                        dismiss();

                    return keyListener.onKey(parent, event.getKeyCode(), event);
                }

                return true;
            }
        };
        cpd.show();
        return true;
    }

    /**
     * Show arrows dialog
     */
    public boolean showArrowsDialog() {
        final String []pickerStrings = {"←", "→", "↑", "↓", "tab", "ins", "del", "ret"};
        final HashMap<String,Integer> keymap = new HashMap<String,Integer>();
        keymap.put("←",   vt320.KEY_LEFT);
        keymap.put("→",   vt320.KEY_RIGHT);
        keymap.put("↑",   vt320.KEY_UP);
        keymap.put("↓",   vt320.KEY_DOWN);
        keymap.put("tab", vt320.KEY_TAB);
        keymap.put("ins", vt320.KEY_INSERT);
        keymap.put("del", vt320.KEY_DELETE);
        keymap.put("ret", vt320.KEY_ENTER);

        CharSequence str = "";
        Editable content = Editable.Factory.getInstance().newEditable(str);

        if (parent == null) return false;

        StringPickerDialog cpd = new StringPickerDialog(parent.getContext(),
                                                        parent, content,
                                                        pickerStrings, true) {
            private void buttonPressed(String s) {
                if (keymap.containsKey(s)) buffer.keyPressed(keymap.get(s), ' ', 0);
            }
            @Override
            public void onItemClick(AdapterView p, View v, int pos, long id) {
                buttonPressed(pickerStrings[pos]);
            }
            @Override
            public void onClick(View v) {
                if (v instanceof Button) {
                    final String s = ((Button) v).getText().toString();
                    if (s.equals("")) dismiss();
                    else buttonPressed(s);
                }
            }
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
                        dismiss();

                    return keyListener.onKey(parent, event.getKeyCode(), event);
                }

                return true;
            }
        };
        cpd.show();
        return true;
    }


    /**
     * CTRL dialog
     */
    private String getCtrlString() {
        final String defaultSet = "ABCDEKLOQRWSTUXZ";
        String set = manager.prefs.getString(PreferenceConstants.CTRL_STRING, defaultSet);

        if (set == null || set.equals("")) {
            set = defaultSet;
        }

        return set;
    }

    public boolean showCtrlDialog() {
        CharSequence str = "";
        Editable content = Editable.Factory.getInstance().newEditable(str);

        if (parent == null)
            return false;

        CharacterPickerDialog cpd = new CharacterPickerDialog(parent.getContext(),
        parent, content, getCtrlString(), true) {
            private void buttonPressed(CharSequence result) {
                int code = result.toString().toUpperCase().charAt(0) - 64;

                if (code > 0 && code < 80) {
                    try {
                        transport.write(code);
                    }
                    catch (IOException e) {
                        Log.d(TAG, "Error writing CTRL+" + result.toString().toUpperCase().charAt(0));
                    }
                }

                dismiss();
            }
            @Override
            public void onItemClick(AdapterView p, View v, int pos, long id) {
                final String result = String.valueOf(getCtrlString().charAt(pos));
                buttonPressed(result);
            }
            @Override
            public void onClick(View v) {
                if (v instanceof Button) {
                    final CharSequence result = ((Button) v).getText();

                    if (result.equals(""))
                        dismiss();
                    else
                        buttonPressed(result);
                }
            }
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
                        dismiss();

                    return keyListener.onKey(parent, event.getKeyCode(), event);
                }

                return true;
            }
        };
        cpd.show();
        return true;
    }

    /**
     * Function keys dialog
     */
    public boolean showFKeysDialog() {
        final String []pickerStrings = {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24"};
        final HashMap<String,Integer> keymap = new HashMap<String,Integer>();
        keymap.put("F1", vt320.KEY_F1);
        keymap.put("F2", vt320.KEY_F2);
        keymap.put("F3", vt320.KEY_F3);
        keymap.put("F4", vt320.KEY_F4);
        keymap.put("F5", vt320.KEY_F5);
        keymap.put("F6", vt320.KEY_F6);
        keymap.put("F7", vt320.KEY_F7);
        keymap.put("F8", vt320.KEY_F8);
        keymap.put("F9", vt320.KEY_F9);
        keymap.put("F10", vt320.KEY_F10);
        keymap.put("F11", vt320.KEY_F11);
        keymap.put("F12", vt320.KEY_F12);
        keymap.put("F13", vt320.KEY_F13);
        keymap.put("F14", vt320.KEY_F14);
        keymap.put("F15", vt320.KEY_F15);
        keymap.put("F16", vt320.KEY_F16);
        keymap.put("F17", vt320.KEY_F17);
        keymap.put("F18", vt320.KEY_F18);
        keymap.put("F19", vt320.KEY_F19);
        keymap.put("F20", vt320.KEY_F20);
        keymap.put("F21", vt320.KEY_F21);
        keymap.put("F22", vt320.KEY_F22);
        keymap.put("F23", vt320.KEY_F23);
        keymap.put("F24", vt320.KEY_F24);

        CharSequence str = "";
        Editable content = Editable.Factory.getInstance().newEditable(str);

        if (parent == null) return false;

        StringPickerDialog cpd = new StringPickerDialog(parent.getContext(),
                                                        parent, content,
                                                        pickerStrings, true) {
            private void buttonPressed(String s) {
                if (keymap.containsKey(s)) buffer.keyPressed(keymap.get(s), ' ', 0);
                dismiss();
            }
            @Override
            public void onItemClick(AdapterView p, View v, int pos, long id) {
                buttonPressed(pickerStrings[pos]);
            }
            @Override
            public void onClick(View v) {
                if (v instanceof Button) {
                    final String s = ((Button) v).getText().toString();
                    if (s.equals("")) dismiss();
                    else buttonPressed(s);
                }
            }
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
                        dismiss();

                    return keyListener.onKey(parent, event.getKeyCode(), event);
                }

                return true;
            }
        };
        cpd.show();
        return true;
    }

    private String getPickerString() {
        final String defaultSet = "~\\^()[]{}<>|/:_;,.!@#$%&*?\"'-+=";
        String set = manager.prefs.getString(PreferenceConstants.PICKER_STRING, defaultSet);

        if (set == null || set.equals("")) {
            set = defaultSet;
        }

        return set;
    }

    public boolean showCharPickerDialog() {
        CharSequence str = "";
        Editable content = Editable.Factory.getInstance().newEditable(str);

        if (parent == null || !transport.isAuthenticated())
            return false;

        CharacterPickerDialog cpd = new CharacterPickerDialog(parent.getContext(),
        parent, content, getPickerString(), true) {
            private void writeChar(CharSequence result) {
                try {
                    if (transport.isAuthenticated())
                        transport.write(result.toString().getBytes(getCharset().name()));
                }
                catch (IOException e) {
                    Log.e(TAG, "Problem with the CharacterPickerDialog", e);
                }

                if (!manager.prefs.getBoolean(PreferenceConstants.PICKER_KEEP_OPEN, false))
                    dismiss();
            }
            @Override
            public void onItemClick(AdapterView p, View v, int pos, long id) {
                String result = String.valueOf(getPickerString().charAt(pos));
                writeChar(result);
            }
            @Override
            public void onClick(View v) {
                if (v instanceof Button) {
                    CharSequence result = ((Button) v).getText();

                    if (result.equals(""))
                        dismiss();
                    else
                        writeChar(result);
                }
            }
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                int keyCode = event.getKeyCode();

                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    // close window if SYM or BACK keys are pressed
                    if (keyListener.isSymKey(keyCode) ||
                            keyCode == KeyEvent.KEYCODE_BACK) {
                        dismiss();
                        return true;
                    }
                }

                return super.dispatchKeyEvent(event);
            }
        };
        cpd.show();
        return true;
    }
}