Mercurial > 510Connectbot
view src/com/five_ten_sg/connectbot/service/TerminalBridge.java @ 260:edf4dacea9ff
allow both fixed rows/columns and specified font size
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Tue, 15 Jul 2014 14:17:22 -0700 |
parents | e13878bb3ddf |
children | 79e6adde6fde |
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(); // initial sequence from // transport.connect() // bridge.onConnected() // bridge.setMyFontSize() // bridge.resizeComputed() // bridge.setFontSize() // bridge.parentChanged() here is on the wrong thread try { parent.notifyUser(String.format("%d x %d", columns, rows)); } catch (Exception e) { Log.e(TAG, "Problem while trying to notify user", e); } 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; boolean fixed = true; if (!fixed) { 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; if (fixed) setFontSize(host.getFontSize()); else 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; } }