/* * 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 org.connectbot.service; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.Bitmap.Config; import android.graphics.Paint.FontMetrics; import android.text.ClipboardManager; import android.view.ContextMenu; import android.view.Menu; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.R; import com.googlecode.android_scripting.facade.ui.UiFacade; import com.googlecode.android_scripting.interpreter.InterpreterProcess; import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager; import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory; import de.mud.terminal.VDUBuffer; import de.mud.terminal.VDUDisplay; import de.mud.terminal.vt320; import java.io.IOException; import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; import org.connectbot.TerminalView; import org.connectbot.transport.AbsTransport; import org.connectbot.util.Colors; import org.connectbot.util.PreferenceConstants; import org.connectbot.util.SelectionArea; /** * 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. * * */ public class TerminalBridge implements VDUDisplay, OnSharedPreferenceChangeListener { private final static int FONT_SIZE_STEP = 2; private final int[] color = new int[Colors.defaults.length]; private final TerminalManager manager; private final InterpreterProcess mProcess; private int mDefaultFgColor; private int mDefaultBgColor; private int scrollback; private String delKey; private String encoding; private AbsTransport transport; private final Paint defaultPaint; private Relay relay; private Bitmap bitmap = null; private final VDUBuffer buffer; private TerminalView parent = null; private final Canvas canvas = new Canvas(); private boolean forcedSize = false; private int columns; private int rows; private final TerminalKeyListener keyListener; private boolean selectingForCopy = false; private final SelectionArea selectionArea; private ClipboardManager clipboard; public int charWidth = -1; public int charHeight = -1; private int charTop = -1; private float fontSize = -1; private final List fontSizeChangedListeners; /** * Flag indicating if we should perform a full-screen redraw during our next rendering pass. */ private boolean fullRedraw = false; private final PromptHelper promptHelper; /** * 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) { } }; manager = null; defaultPaint = new Paint(); selectionArea = new SelectionArea(); scrollback = 1; fontSizeChangedListeners = new LinkedList(); transport = null; keyListener = new TerminalKeyListener(manager, this, buffer, null); mProcess = null; mDefaultFgColor = 0; mDefaultBgColor = 0; promptHelper = null; updateCharset(); } /** * Create new terminal bridge with following parameters. */ public TerminalBridge(final TerminalManager manager, InterpreterProcess process, AbsTransport t) throws IOException { this.manager = manager; transport = t; mProcess = process; String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null); if (string != null) { scrollback = Integer.parseInt(string); } else { scrollback = PreferenceConstants.DEFAULT_SCROLLBACK; } string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null); if (string != null) { fontSize = Float.parseFloat(string); } else { fontSize = PreferenceConstants.DEFAULT_FONT_SIZE; } mDefaultFgColor = manager.getIntParameter(PreferenceConstants.COLOR_FG, PreferenceConstants.DEFAULT_FG_COLOR); mDefaultBgColor = manager.getIntParameter(PreferenceConstants.COLOR_BG, PreferenceConstants.DEFAULT_BG_COLOR); delKey = manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL); // 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? fontSizeChangedListeners = new LinkedList(); setFontSize(fontSize); // create terminal buffer and handle outgoing data // this is probably status reply information buffer = new vt320() { @Override public void debug(String s) { Log.d(s); } @Override public void write(byte[] b) { try { if (b != null && transport != null) { transport.write(b); } } catch (IOException e) { Log.e("Problem writing outgoing data in vt320() thread", e); } } @Override public void write(int b) { try { if (transport != null) { transport.write(b); } } catch (IOException e) { Log.e("Problem writing outgoing data in vt320() thread", e); } } // We don't use telnet sequences. @Override public void sendTelnetCommand(byte cmd) { } // We don't want remote to resize our window. @Override public void setWindowSize(int c, int r) { } @Override public void beep() { if (parent.isShown()) { manager.playBeep(); } } }; // Don't keep any scrollback if a session is not being opened. buffer.setBufferSize(scrollback); resetColors(); buffer.setDisplay(this); selectionArea = new SelectionArea(); keyListener = new TerminalKeyListener(manager, this, buffer, encoding); updateCharset(); manager.registerOnSharedPreferenceChangeListener(this); } /** * Spawn thread to open connection and start login process. */ protected void connect() { transport.setBridge(this); transport.setManager(manager); transport.connect(); ((vt320) buffer).reset(); // previously tried vt100 and xterm for emulation modes // "screen" works the best for color and escape codes ((vt320) buffer).setAnswerBack("screen"); if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) { ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE); } else { ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL); } // create thread to relay incoming connection data to buffer relay = new Relay(this, transport, (vt320) buffer, encoding); Thread relayThread = new Thread(relay); relayThread.setDaemon(true); relayThread.setName("Relay"); relayThread.start(); // force font-size to make sure we resizePTY as needed setFontSize(fontSize); } private void updateCharset() { encoding = manager.getStringParameter(PreferenceConstants.ENCODING, Charset.defaultCharset().name()); if (relay != null) { relay.setCharset(encoding); } keyListener.setCharset(encoding); } /** * 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(encoding)); } catch (Exception e) { Log.e("Couldn't inject string to remote host: ", e); } } }); injectStringThread.setName("InjectString"); injectStringThread.start(); } /** * @return whether a session is open or not */ public boolean isSessionOpen() { if (transport != null) { return transport.isSessionOpen(); } return false; } /** * Force disconnection of this terminal bridge. */ public void dispatchDisconnect(boolean immediate) { // Cancel any pending prompts. promptHelper.cancelPrompt(); if (immediate) { manager.closeConnection(TerminalBridge.this, true); } else { Thread disconnectPromptThread = new Thread(new Runnable() { public void run() { String prompt = null; if (transport != null && transport.isConnected()) { prompt = manager.getResources().getString(R.string.prompt_confirm_exit); } else { prompt = manager.getResources().getString(R.string.prompt_process_exited); } Boolean result = promptHelper.requestBooleanPrompt(null, prompt); if (transport != null && transport.isConnected()) { manager.closeConnection(TerminalBridge.this, result != null && result.booleanValue()); } else if (result != null && result.booleanValue()) { manager.closeConnection(TerminalBridge.this, false); } } }); disconnectPromptThread.setName("DisconnectPrompt"); disconnectPromptThread.setDaemon(true); disconnectPromptThread.start(); } } 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. */ /* package */final void setFontSize(float size) { if (size <= 0.0) { return; } defaultPaint.setTextSize(size); fontSize = size; // read new metrics to get exact pixel dimensions FontMetrics fm = defaultPaint.getFontMetrics(); charTop = (int) Math.ceil(fm.top); float[] widths = new float[1]; defaultPaint.getTextWidths("X", widths); charWidth = (int) Math.ceil(widths[0]); charHeight = (int) Math.ceil(fm.descent - fm.top); // refresh any bitmap with new font size if (parent != null) { parentChanged(parent); } for (FontSizeChangedListener ofscl : fontSizeChangedListeners) { ofscl.onFontSizeChanged(size); } forcedSize = false; } /** * 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("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 synchronized (buffer) { buffer.setScreenSize(columns, rows, true); } if (transport != null) { transport.setDimensions(columns, rows, width, height); } } catch (Exception e) { Log.e("Problem while trying to resize screen or PTY", e); } // force full redraw with new buffer size fullRedraw = true; redraw(); parent.notifyUser(String.format("%d x %d", columns, rows)); Log.i(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 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]; // check if foreground color attribute is set if ((currAttr & VDUBuffer.COLOR_FG) != 0) { int 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]; } } else { fg = mDefaultFgColor; } // 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 = mDefaultBgColor; } // 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(String.format("Fontsize: 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; } columns = cols; this.rows = rows; setFontSize(size); forcedSize = true; } 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) Math.ceil(fm.descent - fm.top) * rows; Log.d(String.format("Fontsize: 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; } /* * (non-Javadoc) * * @see de.mud.terminal.VDUDisplay#setVDUBuffer(de.mud.terminal.VDUBuffer) */ @Override public void setVDUBuffer(VDUBuffer buffer) { } /* * (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() { System.arraycopy(Colors.defaults, 0, color, 0, Colors.defaults.length); } 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_STEP); } public void decreaseFontSize() { setFontSize(fontSize - FONT_SIZE_STEP); } public int getId() { return mProcess.getPort(); } public String getName() { return mProcess.getName(); } public InterpreterProcess getProcess() { return mProcess; } public int getForegroundColor() { return mDefaultFgColor; } public int getBackgroundColor() { return mDefaultBgColor; } public VDUBuffer getVDUBuffer() { return buffer; } public PromptHelper getPromptHelper() { return promptHelper; } public Bitmap getBitmap() { return bitmap; } public AbsTransport getTransport() { return transport; } public Paint getPaint() { return defaultPaint; } public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (mProcess.isAlive()) { RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory(); for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) { UiFacade facade = manager.getReceiver(UiFacade.class); facade.onCreateContextMenu(menu, v, menuInfo); } } } public boolean onPrepareOptionsMenu(Menu menu) { boolean returnValue = false; if (mProcess.isAlive()) { RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory(); for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) { UiFacade facade = manager.getReceiver(UiFacade.class); returnValue = returnValue || facade.onPrepareOptionsMenu(menu); } return returnValue; } return false; } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PreferenceConstants.ENCODING.equals(key)) { updateCharset(); } else if (PreferenceConstants.FONTSIZE.equals(key)) { String string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null); if (string != null) { fontSize = Float.parseFloat(string); } else { fontSize = PreferenceConstants.DEFAULT_FONT_SIZE; } setFontSize(fontSize); fullRedraw = true; } else if (PreferenceConstants.SCROLLBACK.equals(key)) { String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null); if (string != null) { scrollback = Integer.parseInt(string); } else { scrollback = PreferenceConstants.DEFAULT_SCROLLBACK; } buffer.setBufferSize(scrollback); } else if (PreferenceConstants.COLOR_FG.equals(key)) { mDefaultFgColor = manager.getIntParameter(PreferenceConstants.COLOR_FG, PreferenceConstants.DEFAULT_FG_COLOR); fullRedraw = true; } else if (PreferenceConstants.COLOR_BG.equals(key)) { mDefaultBgColor = manager.getIntParameter(PreferenceConstants.COLOR_BG, PreferenceConstants.DEFAULT_BG_COLOR); fullRedraw = true; } if (PreferenceConstants.DELKEY.equals(key)) { delKey = manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL); if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) { ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE); } else { ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL); } } } }