diff options
author | Jeffrey Sharkey <jsharkey@jsharkey.org> | 2008-08-27 10:47:56 +0000 |
---|---|---|
committer | Jeffrey Sharkey <jsharkey@jsharkey.org> | 2008-08-27 10:47:56 +0000 |
commit | cbf1af86640c76facfc140b5dfb83f2393c02d19 (patch) | |
tree | 0af4a26136f833765cc444cbfeaa52c465c83c81 /src | |
parent | ba0d4f5a28170e52956705fe75a6763ce79e6264 (diff) | |
download | connectbot-cbf1af86640c76facfc140b5dfb83f2393c02d19.tar.gz connectbot-cbf1af86640c76facfc140b5dfb83f2393c02d19.tar.bz2 connectbot-cbf1af86640c76facfc140b5dfb83f2393c02d19.zip |
* moved all terminal logic into a Service backend. connections are held in place by a TerminalBridge, which keeps the connection alive and renders the screen to a
bitmap if provided. a Console creates TerminalViews for each bridge while it is active, and handles panning back/forth between them.
* volume up/down controls will change console font size
* extended trilead library to support resizePTY() command
* left/right screen gestures will pan between various open consoles
* up/down screen gestures on right-half will look through scrollback buffer
* up/down screen gestures on left-half will trigger pageup/down keys
* broke ctrl+ keyboard mapping, will need to bring back over from older code
Diffstat (limited to 'src')
22 files changed, 1544 insertions, 118 deletions
diff --git a/src/com/trilead/ssh2/Session.java b/src/com/trilead/ssh2/Session.java index 4784537..c41d837 100644 --- a/src/com/trilead/ssh2/Session.java +++ b/src/com/trilead/ssh2/Session.java @@ -129,6 +129,17 @@ public class Session cm.requestPTY(cn, term, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels,
terminal_modes);
}
+
+ public void resizePTY(int width, int height) throws IOException {
+ synchronized (this)
+ {
+ /* The following is just a nicer error, we would catch it anyway later in the channel code */
+ if (flag_closed)
+ throw new IOException("This session is closed.");
+ }
+
+ cm.resizePTY(cn, width, height);
+ }
/**
* Request X11 forwarding for the current session.
diff --git a/src/com/trilead/ssh2/channel/ChannelManager.java b/src/com/trilead/ssh2/channel/ChannelManager.java index ebd7585..74906d3 100644 --- a/src/com/trilead/ssh2/channel/ChannelManager.java +++ b/src/com/trilead/ssh2/channel/ChannelManager.java @@ -17,6 +17,7 @@ import com.trilead.ssh2.packets.PacketOpenDirectTCPIPChannel; import com.trilead.ssh2.packets.PacketOpenSessionChannel;
import com.trilead.ssh2.packets.PacketSessionExecCommand;
import com.trilead.ssh2.packets.PacketSessionPtyRequest;
+import com.trilead.ssh2.packets.PacketSessionPtyResize;
import com.trilead.ssh2.packets.PacketSessionStartShell;
import com.trilead.ssh2.packets.PacketSessionSubsystemRequest;
import com.trilead.ssh2.packets.PacketSessionX11Request;
@@ -675,6 +676,35 @@ public class ChannelManager implements MessageHandler throw (IOException) new IOException("PTY request failed").initCause(e);
}
}
+
+
+ public void resizePTY(Channel c, int width, int height) throws IOException {
+ PacketSessionPtyResize spr;
+
+ synchronized (c) {
+ if (c.state != Channel.STATE_OPEN)
+ throw new IOException("Cannot request PTY on this channel ("
+ + c.getReasonClosed() + ")");
+
+ spr = new PacketSessionPtyResize(c.remoteID, true, width, height);
+ c.successCounter = c.failedCounter = 0;
+ }
+
+ synchronized (c.channelSendLock) {
+ if (c.closeMessageSent)
+ throw new IOException("Cannot request PTY on this channel ("
+ + c.getReasonClosed() + ")");
+ tm.sendMessage(spr.getPayload());
+ }
+
+ try {
+ waitForChannelSuccessOrFailure(c);
+ } catch (IOException e) {
+ throw (IOException) new IOException("PTY request failed")
+ .initCause(e);
+ }
+ }
+
public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol,
String x11AuthenticationCookie, int x11ScreenNumber) throws IOException
diff --git a/src/com/trilead/ssh2/packets/PacketSessionPtyResize.java b/src/com/trilead/ssh2/packets/PacketSessionPtyResize.java new file mode 100644 index 0000000..20d35cd --- /dev/null +++ b/src/com/trilead/ssh2/packets/PacketSessionPtyResize.java @@ -0,0 +1,40 @@ +package com.trilead.ssh2.packets; + +public class PacketSessionPtyResize { + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String term; + public int width; + public int height; + + public PacketSessionPtyResize(int recipientChannelID, boolean wantReply, int width, int height) { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.term = term; + this.width = width; + this.height = height; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("window-change"); + tw.writeBoolean(wantReply); + tw.writeUINT32(width); + tw.writeUINT32(height); + tw.writeUINT32(0); + tw.writeUINT32(0); + + payload = tw.getBytes(); + } + return payload; + } +} + + diff --git a/src/de/mud/terminal/VDUBuffer.java b/src/de/mud/terminal/VDUBuffer.java index 50a2268..120f093 100644 --- a/src/de/mud/terminal/VDUBuffer.java +++ b/src/de/mud/terminal/VDUBuffer.java @@ -339,7 +339,7 @@ public class VDUBuffer { (topMargin > 0 ? topMargin - 1 : 0) : bottomMargin)); - // System.out.println("l is "+l+", top is "+top+", bottom is "+bottom+", bottomargin is "+bottomMargin+", topMargin is "+topMargin); + System.out.println("l is "+l+", top is "+top+", bottom is "+bottom+", bottomargin is "+bottomMargin+", topMargin is "+topMargin); if (scrollDown) { if (n > (bottom - top)) n = (bottom - top); @@ -617,6 +617,7 @@ public class VDUBuffer { * @param l line that is the margin */ public void setTopMargin(int l) { + //System.out.println("trying setTopMargin " + l); if (l > bottomMargin) { topMargin = bottomMargin; bottomMargin = l; @@ -624,6 +625,7 @@ public class VDUBuffer { topMargin = l; if (topMargin < 0) topMargin = 0; if (bottomMargin > height - 1) bottomMargin = height - 1; + //System.out.println("final setTopMargin " + topMargin); } /** @@ -640,6 +642,7 @@ public class VDUBuffer { * @param l line that is the margin */ public void setBottomMargin(int l) { + //System.out.println("trying setBottomMargin " + l); if (l < topMargin) { bottomMargin = topMargin; topMargin = l; @@ -647,6 +650,7 @@ public class VDUBuffer { bottomMargin = l; if (topMargin < 0) topMargin = 0; if (bottomMargin > height - 1) bottomMargin = height - 1; + //System.out.println("final setBottomMargin " + bottomMargin); } /** @@ -756,6 +760,7 @@ public class VDUBuffer { bottomMargin = h - 1; update = new boolean[h + 1]; update[0] = true; + /* FIXME: ??? if(resizeStrategy == RESIZE_FONT) setBounds(getBounds()); diff --git a/src/de/mud/terminal/vt320.java b/src/de/mud/terminal/vt320.java index 5e0f4c2..fccd56a 100644 --- a/src/de/mud/terminal/vt320.java +++ b/src/de/mud/terminal/vt320.java @@ -39,6 +39,7 @@ public abstract class vt320 extends VDUBuffer implements VDUInput { /** The current version id tag.<P> * $Id: vt320.java 507 2005-10-25 10:14:52Z marcus $ + * */ public final static String ID = "$Id: vt320.java 507 2005-10-25 10:14:52Z marcus $"; @@ -1930,7 +1931,7 @@ public abstract class vt320 extends VDUBuffer implements VDUInput { break; case 'r': // XTERM_RESTORE if (true || debug > 1) - System.out.println("ESC [ ? " + DCEvars[0] + " r"); + System.out.println("m1ESC [ ? " + DCEvars[0] + " r"); /* DEC Mode reset */ for (int i = 0; i <= DCEvar; i++) { switch (DCEvars[i]) { @@ -2328,7 +2329,7 @@ public abstract class vt320 extends VDUBuffer implements VDUInput { int limit; /* FIXME: xterm only cares about 0 and topmargin */ if (R > bm) - limit = bm; // BUGFIX: corrects scrollback dissapearing in irssi + limit = bm; // BUGFIX: corrects scrollback dissapearing in irssi (?) else if (R >= tm) { limit = tm; } else @@ -2403,15 +2404,17 @@ public abstract class vt320 extends VDUBuffer implements VDUInput { } else R = rows - 1; setBottomMargin(R); + System.out.println("setBottomMargin R="+R); if (R >= DCEvars[0]) { R = DCEvars[0] - 1; if (R < 0) R = 0; } setTopMargin(R); + System.out.println("setTopMargin R="+R); _SetCursor(0, 0); if (debug > 1) - System.out.println("ESC [" + DCEvars[0] + " ; " + DCEvars[1] + " r"); + System.out.println("m2ESC [" + DCEvars[0] + " ; " + DCEvars[1] + " r"); break; case 'G': /* CUP / cursor absolute column */ C = DCEvars[0]; @@ -2682,7 +2685,7 @@ public abstract class vt320 extends VDUBuffer implements VDUInput { if (debug > 3) System.out.print("" + DCEvars[i] + ";"); } - if (debug > 3) + if (debug > 4) System.out.print(" (attributes = " + attributes + ")m \n"); break; default: diff --git a/src/org/connectbot/Console.java b/src/org/connectbot/Console.java new file mode 100644 index 0000000..2895ed5 --- /dev/null +++ b/src/org/connectbot/Console.java @@ -0,0 +1,250 @@ +package org.connectbot; + +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalBridgeSurface; +import org.connectbot.service.TerminalManager; +import org.theb.ssh.InteractiveHostKeyVerifier; + +import org.theb.ssh.R; +import com.trilead.ssh2.Connection; + +import de.mud.terminal.vt320; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.View.OnKeyListener; +import android.view.View.OnTouchListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Animation.AnimationListener; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.ViewFlipper; +import android.widget.LinearLayout.LayoutParams; + +public class Console extends Activity { + + public ViewFlipper flip = null; + public TerminalManager bound = null; + public LayoutInflater inflater = null; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + bound = ((TerminalManager.TerminalBinder) service).getService(); + + Log.d(this.getClass().toString(), "ohhai there, i found bridges=" + bound.bridges.size()); + + // clear out any existing bridges and record requested index + flip.removeAllViews(); + int requestedIndex = 0; + + // create views for all bridges on this service + for(TerminalBridge bridge : bound.bridges) { + + // inflate each terminal view + RelativeLayout view = (RelativeLayout)inflater.inflate(R.layout.item_terminal, flip, false); + + // set the terminal overlay text + TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay); + overlay.setText(bridge.overlay); + + // and add our terminal view control, using index to place behind overlay + TerminalView terminal = new TerminalView(Console.this, bridge); + terminal.setId(R.id.console_flip); + view.addView(terminal, 0); + + // finally attach to the flipper + flip.addView(view); + + // check to see if this bridge was requested + if(bridge.overlay.equals(requestedBridge)) + requestedIndex = flip.getChildCount() - 1; + + } + + // show the requested bridge if found, also fade out overlay + flip.setDisplayedChild(requestedIndex); + flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out); + + } + + public void onServiceDisconnected(ComponentName className) { + // remove all bridge views + bound = null; + flip.removeAllViews(); + } + }; + + public Animation fade_out = null; + public String requestedBridge = null; + + public void updateDefault() { + // update the current default terminal + TerminalView terminal = (TerminalView)flip.getCurrentView().findViewById(R.id.console_flip); + if(bound == null || terminal == null) return; + bound.defaultBridge = terminal.bridge; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + this.requestWindowFeature(Window.FEATURE_NO_TITLE); + this.setContentView(R.layout.act_console); + + // pull out any requested bridge + this.requestedBridge = this.getIntent().getExtras().getString(Intent.EXTRA_TEXT); + + this.inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.flip = (ViewFlipper)this.findViewById(R.id.console_flip); + + this.fade_out = AnimationUtils.loadAnimation(this, R.anim.fade_out); + + // connect with manager service to find all bridges + // when connected it will insert all views + this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + + // preload animations for terminal switching + final Animation slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in); + final Animation slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out); + final Animation slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in); + final Animation slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out); + final Animation fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden); + + // detect fling gestures to switch between terminals + final GestureDetector detect = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + + public float totalY = 0; + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + // if releasing then reset total scroll + if(e2.getAction() == MotionEvent.ACTION_UP) { + totalY = 0; + } + + // activate consider if within x tolerance + if(Math.abs(e1.getX() - e2.getX()) < 100) { + + TerminalView terminal = (TerminalView)flip.getCurrentView().findViewById(R.id.console_flip); + + // estimate how many rows we have scrolled through + // accumulate distance that doesn't trigger immediate scroll + totalY += distanceY; + int moved = (int)(totalY / terminal.bridge.charHeight); + + // consume as scrollback only if towards right half of screen + if(e2.getX() > flip.getWidth() / 2) { + if(moved != 0) { + int base = terminal.bridge.buffer.getWindowBase(); + terminal.bridge.buffer.setWindowBase(base + moved); + totalY = 0; + return true; + } + } else { + // otherwise consume as pgup/pgdown for every 5 lines + if(moved > 5) { + Log.d(this.getClass().toString(), "going pagedown"); + ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0); + totalY = 0; + return true; + } else if(moved < -5) { + Log.d(this.getClass().toString(), "going pageup"); + ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0); + totalY = 0; + return true; + } + + } + + + } + + return false; + } + + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + float distx = e2.getRawX() - e1.getRawX(); + float disty = e2.getRawY() - e1.getRawY(); + int goalwidth = flip.getWidth() / 2; + int goalheight = flip.getHeight() / 2; + + // need to slide across half of display to trigger console change + // make sure user kept a steady hand horizontally + if(Math.abs(disty) < 100) { + if(distx > goalwidth) { + + // keep current overlay from popping up again + flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_stay_hidden); + + flip.setInAnimation(slide_right_in); + flip.setOutAnimation(slide_right_out); + flip.showPrevious(); + Console.this.updateDefault(); + + // show overlay on new slide and start fade + flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out); + return true; + } + + if(distx < -goalwidth) { + + // keep current overlay from popping up again + flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_stay_hidden); + + flip.setInAnimation(slide_left_in); + flip.setOutAnimation(slide_left_out); + flip.showNext(); + Console.this.updateDefault(); + + // show overlay on new slide and start fade + flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out); + return true; + } + + } + + + + return false; + } + + + }); + + + flip.setLongClickable(true); + flip.setOnTouchListener(new OnTouchListener() { + + public boolean onTouch(View v, MotionEvent event) { + // pass any touch events back to detector + return detect.onTouchEvent(event); + } + + }); + + + + + } + +} diff --git a/src/org/connectbot/FrontPage.java b/src/org/connectbot/FrontPage.java new file mode 100644 index 0000000..4fa7734 --- /dev/null +++ b/src/org/connectbot/FrontPage.java @@ -0,0 +1,133 @@ +package org.connectbot; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.theb.ssh.InteractiveHostKeyVerifier; + +import com.trilead.ssh2.Connection; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; +import org.theb.ssh.R; + +public class FrontPage extends Activity { + + public TerminalManager bound = null; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + bound = ((TerminalManager.TerminalBinder) service).getService(); + + // TODO: update our green bulb icons by checking for existing bridges + // open up some test sessions +// try { +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// } catch(Exception e) { +// e.printStackTrace(); +// } + + } + + public void onServiceDisconnected(ComponentName className) { + bound = null; + } + }; + + + public final static String ITEM_TITLE = "title"; + public final static String ITEM_CAPTION = "caption"; + public final static String ITEM_IMAGE = "image"; + + public Map<String,?> createItem(String title, String caption, int image) { + Map<String,String> item = new HashMap<String,String>(); + item.put(ITEM_TITLE, title); + item.put(ITEM_CAPTION, caption); + item.put(ITEM_IMAGE, Integer.toString(image)); + return item; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_frontpage); + + // start the terminal manager service + this.startService(new Intent(this, TerminalManager.class)); + this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + + + // create some test hostmasks + List<Map<String,?>> security = new LinkedList<Map<String,?>>(); + security.add(createItem("user@example.org", "20 minutes ago, connected", android.R.drawable.presence_online)); + security.add(createItem("root@home.example.com", "1 hour ago, connected", android.R.drawable.presence_online)); + security.add(createItem("person@192.168.0.1", "12 days ago", android.R.drawable.presence_invisible)); + security.add(createItem("root@google.com", "never", android.R.drawable.presence_invisible)); + security.add(createItem("nobody@example.net", "14 years ago, broken socket", android.R.drawable.presence_busy)); + security.add(createItem("root@home.example.com", "1 hour ago", android.R.drawable.presence_invisible)); + security.add(createItem("person@192.168.0.1", "12 minutes ago", android.R.drawable.presence_invisible)); + + final ListView list = (ListView)this.findViewById(R.id.front_hostlist); + list.setAdapter(new SimpleAdapter(this, security, R.layout.item_host, new String[] { ITEM_TITLE, ITEM_CAPTION, ITEM_IMAGE }, new int[] { R.id.host_title, R.id.host_caption, R.id.host_connected })); + + list.setOnItemClickListener(new OnItemClickListener() { + + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + + // TODO: actually perform connection process here + // launch off to console details + FrontPage.this.startActivity(new Intent(FrontPage.this, Console.class)); + + } + + }); + + +// final TextView text = (TextView)this.findViewById(R.id.front_quickconnect); +// text.setOnKeyListener(new OnKeyListener() { +// +// public boolean onKey(View v, int keyCode, KeyEvent event) { +// +// // set list filter based on text +// String filter = text.getText().toString(); +// list.setTextFilterEnabled((filter.length() > 0)); +// list.setFilterText(filter); +// +// // TODO Auto-generated method stub +// return false; +// } +// +// }); + + + + + + + + + + } + +} diff --git a/src/org/connectbot/HostEditor.java b/src/org/connectbot/HostEditor.java new file mode 100644 index 0000000..373897d --- /dev/null +++ b/src/org/connectbot/HostEditor.java @@ -0,0 +1,15 @@ +package org.connectbot; + +import android.app.Activity; +import android.os.Bundle; +import org.theb.ssh.R; + +public class HostEditor extends Activity { + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_hosteditor); + } + +} diff --git a/src/org/connectbot/TerminalView.java b/src/org/connectbot/TerminalView.java new file mode 100644 index 0000000..0e7e283 --- /dev/null +++ b/src/org/connectbot/TerminalView.java @@ -0,0 +1,56 @@ +package org.connectbot; + +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalBridgeSurface; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +public class TerminalView extends View { + + public final Context context; + public final TerminalBridge bridge; + public final Paint paint; + + public TerminalView(Context context, TerminalBridge bridge) { + super(context); + + this.context = context; + this.bridge = bridge; + this.paint = new Paint(); + + this.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + this.setFocusable(true); + this.setFocusableInTouchMode(true); + + // connect our view up to the bridge + this.setOnKeyListener(bridge); + + } + + public void destroy() { + // tell bridge to destroy its bitmap + this.bridge.parentDestroyed(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + this.bridge.parentChanged(this); + } + + @Override + public void onDraw(Canvas canvas) { + // draw the bridge if it exists + if(this.bridge.bitmap != null) { + canvas.drawBitmap(this.bridge.bitmap, 0, 0, this.paint); + } + + } + +} diff --git a/src/org/connectbot/TerminalViewSurface.java b/src/org/connectbot/TerminalViewSurface.java new file mode 100644 index 0000000..cb68696 --- /dev/null +++ b/src/org/connectbot/TerminalViewSurface.java @@ -0,0 +1,37 @@ +package org.connectbot; + +import org.connectbot.service.TerminalBridgeSurface; + +import android.content.Context; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup.LayoutParams; + +public class TerminalViewSurface extends SurfaceView { + + public final Context context; + public final TerminalBridgeSurface bridge; + + public TerminalViewSurface(Context context, TerminalBridgeSurface bridge) { + super(context); + + this.context = context; + this.bridge = bridge; + + this.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + this.setFocusable(true); + this.setFocusableInTouchMode(true); + + + // connect our surface up to the bridge + this.getHolder().setType(SurfaceHolder.SURFACE_TYPE_HARDWARE); + this.getHolder().addCallback(bridge); + this.setOnKeyListener(bridge); + + } + + // TODO: make sure we pass any keystrokes down to bridge + + +} diff --git a/src/org/connectbot/service/TerminalBridge.java b/src/org/connectbot/service/TerminalBridge.java new file mode 100644 index 0000000..ed2424c --- /dev/null +++ b/src/org/connectbot/service/TerminalBridge.java @@ -0,0 +1,381 @@ +package org.connectbot.service; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.theb.ssh.JTATerminalView; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.Bitmap.Config; +import android.graphics.Paint.FontMetricsInt; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnKeyListener; + +import com.trilead.ssh2.Session; +import com.trilead.ssh2.Connection; + +import de.mud.terminal.VDUBuffer; +import de.mud.terminal.VDUDisplay; +import de.mud.terminal.vt320; + + +// provides a bridge between the mud buffer and a surfaceholder +public class TerminalBridge implements VDUDisplay, OnKeyListener { + + public final Connection connection; + public final Session session; + public final String overlay; + + public final static int TERM_WIDTH_CHARS = 80, + TERM_HEIGHT_CHARS = 24, + DEFAULT_FONT_SIZE = 10; + + public final static String ENCODING = "ASCII"; + + public final OutputStream stdin; + public final InputStream stdout; + + public final Paint defaultPaint; + + public final Thread relay; + + public View parent = null; + public Bitmap bitmap = null; + public Canvas canvas = new Canvas(); + public VDUBuffer buffer = null; + + public TerminalBridge(Connection connection, String overlay, String emulation, int scrollback) throws Exception { + // create a terminal bridge from an SSH connection over to a SurfaceHolder + // will open a new session and handle rendering to the Surface if present + + try { + this.connection = connection; + this.session = connection.openSession(); + this.session.requestPTY(emulation, 0, 0, 0, 0, null); // previously tried vt100, xterm, but "screen" works the best + this.session.startShell(); + + this.overlay = overlay; + + // grab stdin/out from newly formed session + this.stdin = this.session.getStdin(); + this.stdout = this.session.getStdout(); + + // create our default paint + this.defaultPaint = new Paint(); + this.defaultPaint.setAntiAlias(true); + this.defaultPaint.setTypeface(Typeface.MONOSPACE); + this.setFontSize(DEFAULT_FONT_SIZE); + + // create terminal buffer and handle outgoing data + // this is probably status reply information + this.buffer = new vt320() { + public void write(byte[] b) { + try { + //Log.d("STDIN", new String(b)); + TerminalBridge.this.stdin.write(b); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void sendTelnetCommand(byte cmd) { + } + + public void setWindowSize(int c, int r) { + } + }; + this.scrollback = scrollback; + this.buffer.setScreenSize(TERM_WIDTH_CHARS, TERM_HEIGHT_CHARS, true); + this.buffer.setBufferSize(scrollback); + this.buffer.setDisplay(this); + + // create thread to relay incoming connection data to buffer + this.relay = new Thread(new Runnable() { + public void run() { + byte[] b = new byte[256]; + int n = 0; + while(n >= 0) { + try { + n = TerminalBridge.this.stdout.read(b); + if(n > 0) { + // pass along data to buffer, then redraw any results + ((vt320)TerminalBridge.this.buffer).putString(new String(b, 0, n, ENCODING)); + TerminalBridge.this.redraw(); + } + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + } + }); + this.relay.start(); + + } catch(Exception e) { + throw e; + } + + for(int i = 0; i < color.length; i++) + this.darkerColor[i] = darken(color[i]); + + } + + public void dispose() { + this.session.close(); + this.connection.close(); + } + + KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + + public boolean onKey(View v, int keyCode, KeyEvent event) { + // pass through any keystrokes to output stream + if(event.getAction() == KeyEvent.ACTION_UP) return false; + try { + + // check for resizing keys + if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + this.setFontSize(this.fontSize + 2); + return true; + } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + this.setFontSize(this.fontSize - 2); + return true; + } + + // print normal keys + if(keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE) { + int key = keymap.get(keyCode, event.getMetaState()); + this.stdin.write(key); + return true; + } + + // look for special chars + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: stdin.write(0x08); return true; + case KeyEvent.KEYCODE_ENTER: ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', event.getMetaState()); return true; + case KeyEvent.KEYCODE_DPAD_LEFT: ((vt320)buffer).keyPressed(vt320.KEY_LEFT, ' ', event.getMetaState()); return true; + case KeyEvent.KEYCODE_DPAD_UP: ((vt320)buffer).keyPressed(vt320.KEY_UP, ' ', event.getMetaState()); return true; + case KeyEvent.KEYCODE_DPAD_DOWN: ((vt320)buffer).keyPressed(vt320.KEY_DOWN, ' ', event.getMetaState()); return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: ((vt320)buffer).keyPressed(vt320.KEY_RIGHT, ' ', event.getMetaState()); return true; + } + + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public int charWidth = -1, + charHeight = -1, + charDescent = -1; + + public float fontSize = -1; + + public int scrollback = 120; + + public void setFontSize(float size) { + this.defaultPaint.setTextSize(size); + this.fontSize = size; + + // read new metrics to get exact pixel dimensions + FontMetricsInt fm = this.defaultPaint.getFontMetricsInt(); + this.charDescent = fm.descent; + + float[] widths = new float[1]; + this.defaultPaint.getTextWidths("X", widths); + this.charWidth = (int)widths[0]; + this.charHeight = Math.abs(fm.top) + Math.abs(fm.descent); + + // refresh any bitmap with new font size + if(this.parent != null) + this.parentChanged(this.parent); + } + + public boolean fullRedraw = false; + + public synchronized void parentChanged(View parent) { + + this.parent = parent; + int width = parent.getWidth(); + int height = parent.getHeight(); + + // reallocate new bitmap if needed + boolean newBitmap = (this.bitmap == null); + if(this.bitmap != null) + newBitmap = (this.bitmap.getWidth() != width || this.bitmap.getHeight() != height); + if(newBitmap) { + this.bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + this.canvas.setBitmap(this.bitmap); + } + + // clear out any old buffer information + this.defaultPaint.setColor(Color.BLACK); + this.canvas.drawRect(0, 0, width, height, this.defaultPaint); + + // recalculate buffer size and update buffer + int termWidth = width / charWidth; + int termHeight = height / charHeight; + + try { + buffer.setScreenSize(termWidth, termHeight, true); + session.resizePTY(termWidth, termHeight); + } catch(Exception e) { + e.printStackTrace(); + } + + // force full redraw with new buffer size + this.fullRedraw = true; + this.redraw(); + + Log.d(this.getClass().toString(), "parentChanged() now width=" + termWidth + ", height=" + termHeight); + } + + public synchronized void parentDestroyed() { + this.parent = null; + this.bitmap = null; + this.canvas.setBitmap(null); + } + + public void setVDUBuffer(VDUBuffer buffer) { + this.buffer = buffer; + } + + public VDUBuffer getVDUBuffer() { + return buffer; + } + + private int darken(int color) { + return Color.argb(0xFF, + (int)(Color.red(color) * 0.8), + (int)(Color.green(color) * 0.8), + (int)(Color.blue(color) * 0.8) + ); + } + + private int color[] = { Color.BLACK, Color.RED, Color.GREEN, Color.YELLOW, + Color.BLUE, Color.MAGENTA, Color.CYAN, Color.WHITE, }; + + private int darkerColor[] = new int[color.length]; + + private final static int COLOR_FG_STD = 7; + private final static int COLOR_BG_STD = 0; + + public long lastDraw = 0; + public long drawTolerance = 100; + + public synchronized void redraw() { + // render our buffer only if we have a surface + if(this.parent == null) return; + + long time = System.currentTimeMillis(); + int lines = 0; + + // only worry about rendering if its been awhile + //if(time - lastDraw < drawTolerance) return; + //lastDraw = time; + + int fg, bg; + boolean entireDirty = buffer.update[0] || this.fullRedraw; + + // walk through all lines in the buffer + for(int l = 0; l < buffer.height; l++) { + +// if(entireDirty) +// Log.w("BUFFERDUMP", new String(buffer.charArray[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; + lines++; + + // reset dirty flag for this line + buffer.update[l + 1] = false; + + // lock this entire row as being dirty + //Rect dirty = new Rect(0, l * charHeight, buffer.width * charWidth, (l + 1) * charHeight); + + // 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]; + + // reset default colors + fg = color[COLOR_FG_STD]; + bg = color[COLOR_BG_STD]; + + // check if foreground color attribute is set + if((currAttr & VDUBuffer.COLOR_FG) != 0) + fg = color[((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1]; + + // check if background color attribute is set + if((currAttr & VDUBuffer.COLOR_BG) != 0) + bg = darkerColor[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1]; + + // support character inversion by swapping background and foreground color + if ((currAttr & VDUBuffer.INVERT) != 0) { + int swapc = bg; + bg = fg; + fg = swapc; + } + + // if black-on-black, try correcting to grey + if(fg == Color.BLACK && bg == Color.BLACK) + fg = Color.GRAY; + + // correctly set bold and underlined attributes if requested + this.defaultPaint.setFakeBoldText((currAttr & VDUBuffer.BOLD) != 0); + this.defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0); + + // 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++; + } + + // clear this dirty area with background color + this.defaultPaint.setColor(bg); + canvas.drawRect(c * charWidth, l * charHeight, (c + addr) * charWidth, (l + 1) * charHeight, this.defaultPaint); + + // write the text string starting at 'c' for 'addr' number of characters + this.defaultPaint.setColor(fg); + if((currAttr & VDUBuffer.INVISIBLE) == 0) + canvas.drawText(buffer.charArray[buffer.windowBase + l], c, + addr, c * charWidth, ((l + 1) * charHeight) - charDescent, + this.defaultPaint); + + // advance to the next text block with different characteristics + c += addr - 1; + } + + // unlock this row and update + //this.current.unlockCanvasAndPost(canvas); + } + + // reset entire-buffer flags + buffer.update[0] = false; + this.fullRedraw = false; + + // dump out rendering statistics + time = System.currentTimeMillis() - time; + //Log.d(this.getClass().toString(), "redraw called and updated lines=" + lines + " taking ms=" + time); + + this.parent.postInvalidate(); + + } + + public void updateScrollBar() { + // TODO Auto-generated method stub + + } + + + +} diff --git a/src/org/connectbot/service/TerminalBridgeSurface.java b/src/org/connectbot/service/TerminalBridgeSurface.java new file mode 100644 index 0000000..3fa0c83 --- /dev/null +++ b/src/org/connectbot/service/TerminalBridgeSurface.java @@ -0,0 +1,310 @@ +package org.connectbot.service; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.theb.ssh.JTATerminalView; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.Paint.FontMetricsInt; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnKeyListener; + +import com.trilead.ssh2.Session; +import com.trilead.ssh2.Connection; + +import de.mud.terminal.VDUBuffer; +import de.mud.terminal.VDUDisplay; +import de.mud.terminal.vt320; + + +// provides a bridge between the mud buffer and a surfaceholder +public class TerminalBridgeSurface implements SurfaceHolder.Callback, VDUDisplay, OnKeyListener { + + public final Connection connection; + public final Session session; + + public final static int TERM_WIDTH_CHARS = 80, + TERM_HEIGHT_CHARS = 24, + DEFAULT_FONT_SIZE = 10; + + public final static String ENCODING = "ASCII"; + + public final OutputStream stdin; + public final InputStream stdout; + + public final Paint defaultPaint; + + public final Thread relay; + + public SurfaceHolder current = null; + public VDUBuffer buffer = null; + + public TerminalBridgeSurface(Connection connection) throws Exception { + // create a terminal bridge from an SSH connection over to a SurfaceHolder + // will open a new session and handle rendering to the Surface if present + + try { + this.connection = connection; + this.session = connection.openSession(); + this.session.requestPTY("xterm", TERM_WIDTH_CHARS, TERM_HEIGHT_CHARS, 0, 0, null); + this.session.startShell(); + + // grab stdin/out from newly formed session + this.stdin = this.session.getStdin(); + this.stdout = this.session.getStdout(); + + // create our default paint + this.defaultPaint = new Paint(); + this.defaultPaint.setAntiAlias(true); + this.defaultPaint.setTypeface(Typeface.MONOSPACE); + this.setFontSize(DEFAULT_FONT_SIZE); + + // create terminal buffer and handle outgoing data + // this is probably status reply information + this.buffer = new vt320() { + public void write(byte[] b) { + try { + Log.d("STDIN", new String(b)); + TerminalBridgeSurface.this.stdin.write(b); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void sendTelnetCommand(byte cmd) { + } + + public void setWindowSize(int c, int r) { + } + }; + this.buffer.setDisplay(this); + + // create thread to relay incoming connection data to buffer + this.relay = new Thread(new Runnable() { + public void run() { + byte[] b = new byte[256]; + int n = 0; + while(n >= 0) { + try { + n = TerminalBridgeSurface.this.stdout.read(b); + if(n > 0) { + // pass along data to buffer, then redraw any results + ((vt320)TerminalBridgeSurface.this.buffer).putString(new String(b, 0, n, ENCODING)); + TerminalBridgeSurface.this.redraw(); + } + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + } + }); + this.relay.start(); + + } catch(Exception e) { + throw e; + } + + } + + KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + + public boolean onKey(View v, int keyCode, KeyEvent event) { + // pass through any keystrokes to output stream + if(event.getAction() == KeyEvent.ACTION_UP) return false; + try { + int key = keymap.get(keyCode, event.getMetaState()); + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: stdin.write(0x08); break; + case KeyEvent.KEYCODE_ENTER: ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', event.getMetaState()); break; + case KeyEvent.KEYCODE_DPAD_LEFT: ((vt320)buffer).keyPressed(vt320.KEY_LEFT, ' ', event.getMetaState()); break; + case KeyEvent.KEYCODE_DPAD_UP: ((vt320)buffer).keyPressed(vt320.KEY_UP, ' ', event.getMetaState()); break; + case KeyEvent.KEYCODE_DPAD_DOWN: ((vt320)buffer).keyPressed(vt320.KEY_DOWN, ' ', event.getMetaState()); break; + case KeyEvent.KEYCODE_DPAD_RIGHT: ((vt320)buffer).keyPressed(vt320.KEY_RIGHT, ' ', event.getMetaState()); break; + default: this.stdin.write(key); + } + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + public int charWidth = -1, + charHeight = -1, + charDescent = -1; + + public void setFontSize(float size) { + this.defaultPaint.setTextSize(size); + + // read new metrics to get exact pixel dimensions + FontMetricsInt fm = this.defaultPaint.getFontMetricsInt(); + this.charDescent = fm.descent; + + float[] widths = new float[1]; + this.defaultPaint.getTextWidths("X", widths); + this.charWidth = (int)widths[0]; + this.charHeight = Math.abs(fm.top) + Math.abs(fm.descent); + + // we probably need to resize the viewport with changed size + // behave just as if the surface changed to reset buffer size + this.surfaceChanged(this.current, -1, -1, -1); + } + + public boolean newSurface = false; + + public synchronized void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + // mark that we need the entire surface repainted + this.current = holder; + this.newSurface = true; + + if(this.current == null) return; + + // resize the underlying buffer as needed + Rect size = this.current.getSurfaceFrame(); + int termWidth = size.width() / charWidth; + int termHeight = size.height() / charHeight; + + buffer.setScreenSize(termWidth, termHeight, true); + buffer.height = termHeight; // TODO: is this really needed? + + Log.d(this.getClass().toString(), "surfaceChanged() now width=" + termWidth + ", height=" + termHeight); + this.redraw(); + + } + + public synchronized void surfaceCreated(SurfaceHolder holder) { + // someone created our Surface, so resize terminal as needed and repaint + // we handle this just like we would a changing surface + this.surfaceChanged(holder, -1, -1, -1); + + } + + public synchronized void surfaceDestroyed(SurfaceHolder holder) { + this.current = null; + this.newSurface = false; + + } + + + public void setVDUBuffer(VDUBuffer buffer) { + this.buffer = buffer; + } + + public VDUBuffer getVDUBuffer() { + return buffer; + } + + private int color[] = { Color.BLACK, Color.RED, Color.GREEN, Color.YELLOW, + Color.BLUE, Color.MAGENTA, Color.CYAN, Color.WHITE, }; + + private int darkerColor[] = color; + + private final static int COLOR_FG_STD = 7; + private final static int COLOR_BG_STD = 0; + + public void redraw() { + // render our buffer only if we have a surface + if(this.current == null) return; + + long time = System.currentTimeMillis(); + int lines = 0; + + int fg, bg; + boolean entireDirty = buffer.update[0] || newSurface; + + // 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; + lines++; + + // reset dirty flag for this line + buffer.update[l + 1] = false; + + // lock this entire row as being dirty + Rect dirty = new Rect(0, l * charHeight, buffer.width * charWidth, (l + 1) * charHeight); + Canvas canvas = this.current.lockCanvas(dirty); + + // 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]; + + // reset default colors + fg = color[COLOR_FG_STD]; + bg = color[COLOR_BG_STD]; + + // check if foreground color attribute is set + if((currAttr & VDUBuffer.COLOR_FG) != 0) + fg = color[((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1]; + + // check if background color attribute is set + if((currAttr & VDUBuffer.COLOR_BG) != 0) + bg = darkerColor[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1]; + + // support character inversion by swapping background and foreground color + if ((currAttr & VDUBuffer.INVERT) != 0) { + int swapc = bg; + bg = fg; + fg = swapc; + } + + // correctly set bold and underlined attributes if requested + this.defaultPaint.setFakeBoldText((currAttr & VDUBuffer.BOLD) != 0); + this.defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0); + + // 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++; + } + + // clear this dirty area with background color + this.defaultPaint.setColor(bg); + canvas.drawRect(c * charWidth, l * charHeight, (c + addr) * charWidth, (l + 1) * charHeight, this.defaultPaint); + + // write the text string starting at 'c' for 'addr' number of characters + this.defaultPaint.setColor(fg); + if((currAttr & VDUBuffer.INVISIBLE) == 0) + canvas.drawText(buffer.charArray[buffer.windowBase + l], c, + addr, c * charWidth, ((l + 1) * charHeight) - charDescent, + this.defaultPaint); + + // advance to the next text block with different characteristics + c += addr - 1; + } + + // unlock this row and update + this.current.unlockCanvasAndPost(canvas); + } + + // reset entire-buffer flags + buffer.update[0] = false; + this.newSurface = false; + + // dump out rendering statistics + time = System.currentTimeMillis() - time; + Log.d(this.getClass().toString(), "redraw called and updated lines=" + lines + " taking ms=" + time); + + } + + public void updateScrollBar() { + // TODO Auto-generated method stub + + } + + + +} diff --git a/src/org/connectbot/service/TerminalManager.java b/src/org/connectbot/service/TerminalManager.java new file mode 100644 index 0000000..72203ba --- /dev/null +++ b/src/org/connectbot/service/TerminalManager.java @@ -0,0 +1,81 @@ +package org.connectbot.service; + +import java.util.LinkedList; +import java.util.List; + +import org.theb.ssh.InteractiveHostKeyVerifier; + +import com.trilead.ssh2.Connection; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +public class TerminalManager extends Service { + + public List<TerminalBridge> bridges = new LinkedList<TerminalBridge>(); + public TerminalBridge defaultBridge = null; + + @Override + public void onCreate() { + Log.w(this.getClass().toString(), "onCreate()"); + } + + @Override + public void onDestroy() { + Log.w(this.getClass().toString(), "onDestroy()"); + // disconnect and dispose of any bridges + for(TerminalBridge bridge : bridges) + bridge.dispose(); + } + + public void openConnection(Connection conn, String nickname, String emulation, int scrollback) throws Exception { + try { + TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback); + this.bridges.add(bridge); + } catch (Exception e) { + throw e; + } + } + + public void openConnection(String nickname, String host, int port, String user, String pass, String emulation, int scrollback) throws Exception { + try { + Connection conn = new Connection(host, port); + conn.connect(new InteractiveHostKeyVerifier()); + if(conn.isAuthMethodAvailable(user, "password")) { + conn.authenticateWithPassword(user, pass); + } + TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback); + this.bridges.add(bridge); + } catch (Exception e) { + throw e; + } + } + + public TerminalBridge findBridge(String nickname) { + // find the first active bridge with given nickname + for(TerminalBridge bridge : bridges) { + if(bridge.overlay.equals(nickname)) + return bridge; + } + return null; + } + + + public class TerminalBinder extends Binder { + public TerminalManager getService() { + return TerminalManager.this; + } + } + + private final IBinder binder = new TerminalBinder(); + + @Override + public IBinder onBind(Intent intent) { + Log.w(this.getClass().toString(), "onBind()"); + return binder; + } + +} diff --git a/src/org/theb/provider/HostDb.java b/src/org/theb/provider/HostDb.java index 3a15c8f..1ed7c0c 100644 --- a/src/org/theb/provider/HostDb.java +++ b/src/org/theb/provider/HostDb.java @@ -27,11 +27,14 @@ public final class HostDb { public static final Uri CONTENT_URI = Uri.parse("content://org.theb.provider.HostDb/hosts"); - public static final String DEFAULT_SORT_ORDER = "hostname DESC"; + public static final String DEFAULT_SORT_ORDER = "nickname DESC"; + public static final String NICKNAME = "nickname"; public static final String USERNAME = "username"; public static final String HOSTNAME = "hostname"; public static final String PORT = "port"; + public static final String EMULATION = "emulation"; + public static final String SCROLLBACK = "scrollback"; public static final String HOSTKEY = "hostkey"; } } diff --git a/src/org/theb/ssh/HostDbProvider.java b/src/org/theb/ssh/HostDbProvider.java index ecb0eed..f3bdbd5 100644 --- a/src/org/theb/ssh/HostDbProvider.java +++ b/src/org/theb/ssh/HostDbProvider.java @@ -43,7 +43,7 @@ public class HostDbProvider extends ContentProvider { private static final String TAG = "HostDbProvider"; private static final String DATABASE_NAME = "ssh_hosts.db"; - private static final int DATABASE_VERSION = 2; + private static final int DATABASE_VERSION = 3; private static HashMap<String, String> HOSTS_LIST_PROJECTION_MAP; @@ -61,9 +61,15 @@ public class HostDbProvider extends ContentProvider { @Override public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE hosts (_id INTEGER PRIMARY KEY," - + "hostname TEXT," + "username TEXT," + "port INTEGER," - + "hostkey TEXT" + ")"); + db.execSQL("CREATE TABLE hosts (_id INTEGER PRIMARY KEY," + + "nickname TEXT," + + "hostname TEXT," + + "username TEXT," + + "port INTEGER," + + "emulation TEXT," + + "scrollback INTEGER," + + "hostkey TEXT" + + ")"); } @Override @@ -127,6 +133,10 @@ public class HostDbProvider extends ContentProvider { throw new IllegalArgumentException("Unknown Insert " + uri); } */ + if (values.containsKey(HostDb.Hosts.NICKNAME) == false) { + values.put(HostDb.Hosts.NICKNAME, ""); + } + if (values.containsKey(HostDb.Hosts.HOSTNAME) == false) { values.put(HostDb.Hosts.HOSTNAME, ""); } @@ -143,6 +153,14 @@ public class HostDbProvider extends ContentProvider { values.put(HostDb.Hosts.HOSTKEY, ""); } + if (values.containsKey(HostDb.Hosts.EMULATION) == false) { + values.put(HostDb.Hosts.EMULATION, ""); + } + + if (values.containsKey(HostDb.Hosts.SCROLLBACK) == false) { + values.put(HostDb.Hosts.SCROLLBACK, ""); + } + rowID = mDB.insert("hosts", "host", values); if (rowID > 0) { Uri newUri = ContentUris.withAppendedId(HostDb.Hosts.CONTENT_URI, rowID); @@ -228,9 +246,12 @@ public class HostDbProvider extends ContentProvider { HOSTS_LIST_PROJECTION_MAP = new HashMap<String, String>(); HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts._ID, "_id"); + HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.NICKNAME, "nickname"); HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.HOSTNAME, "hostname"); HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.USERNAME, "username"); HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.PORT, "port"); HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.HOSTKEY, "hostkey"); + HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.EMULATION, "emulation"); + HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.SCROLLBACK, "scrollback"); } } diff --git a/src/org/theb/ssh/HostEditor.java b/src/org/theb/ssh/HostEditor.java index d9618eb..ed0d101 100644 --- a/src/org/theb/ssh/HostEditor.java +++ b/src/org/theb/ssh/HostEditor.java @@ -18,6 +18,7 @@ */ package org.theb.ssh; +import org.theb.ssh.R; import org.theb.provider.HostDb; import android.app.Activity; @@ -31,29 +32,26 @@ import android.view.WindowManager; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; +import android.widget.Spinner; public class HostEditor extends Activity { - public static final String EDIT_HOST_ACTION = - "com.theb.ssh.action.EDIT_HOST"; + public static final String EDIT_HOST_ACTION = "com.theb.ssh.action.EDIT_HOST"; - private static final String[] PROJECTION = new String[] { - HostDb.Hosts._ID, // 0 - HostDb.Hosts.HOSTNAME, // 1 - HostDb.Hosts.USERNAME, // 2 - HostDb.Hosts.PORT, // 3 - HostDb.Hosts.HOSTKEY, // 4 - }; + private static final String[] PROJECTION = new String[] { HostDb.Hosts._ID, + HostDb.Hosts.NICKNAME, HostDb.Hosts.HOSTNAME, + HostDb.Hosts.USERNAME, HostDb.Hosts.PORT, HostDb.Hosts.EMULATION, + HostDb.Hosts.SCROLLBACK, }; + + private static final int INDEX_NICKNAME = 1, INDEX_HOSTNAME = 2, + INDEX_USERNAME = 3, INDEX_PORT = 4, INDEX_EMULATION = 5, + INDEX_SCROLLBACK = 6; - static final int HOSTNAME_INDEX = 1; - private static final int USERNAME_INDEX = 2; - private static final int PORT_INDEX = 3; // Set up distinct states that the activity can be run in. private static final int STATE_EDIT = 0; private static final int STATE_INSERT = 1; - private EditText mHostname; - private EditText mUsername; - private EditText mPort; + private EditText mNickname, mHostname, mUsername, mPort, mScrollback; + private Spinner mEmulation; // Cursor that will provide access to the host data we are editing private Cursor mCursor; @@ -64,37 +62,20 @@ public class HostEditor extends Activity { @Override public void onCreate(Bundle savedValues) { super.onCreate(savedValues); - - // TODO: update or remove - // Have the system blur any windows behind this one. - //getWindow().setFlags(WindowManager.LayoutParams.BLUR_BEHIND_FLAG, - // WindowManager.LayoutParams.BLUR_BEHIND_FLAG); - - // Apply a tint to any windows behind this one. Doing a tint this - // way is more efficient than using a translucent background. Note - // that the tint color really should come from a resource. - WindowManager.LayoutParams lp = getWindow().getAttributes(); - //lp.tintBehind = 0x60000820; - getWindow().setAttributes(lp); - - this.setContentView(R.layout.host_editor); + this.setContentView(R.layout.act_hosteditor); // Set up click handlers for text fields and button - mHostname = (EditText) findViewById(R.id.hostname); - mUsername = (EditText) findViewById(R.id.username); - mPort = (EditText) findViewById(R.id.port); - - Button addButton = (Button) findViewById(R.id.add); - addButton.setOnClickListener(mCommitListener); - - Button cancelButton = (Button) findViewById(R.id.cancel); - cancelButton.setOnClickListener(mCancelListener); - - final Intent intent = getIntent(); + this.mNickname = (EditText) findViewById(R.id.edit_nickname); + this.mHostname = (EditText) findViewById(R.id.edit_hostname); + this.mUsername = (EditText) findViewById(R.id.edit_username); + this.mPort = (EditText) findViewById(R.id.edit_port); + this.mEmulation = (Spinner) findViewById(R.id.edit_emulation); + this.mScrollback = (EditText) findViewById(R.id.edit_scrollback); // Do some setup based on the action being performed. - + final Intent intent = getIntent(); final String action = intent.getAction(); + if (Intent.ACTION_INSERT.equals(action)) { mState = STATE_INSERT; mURI = getContentResolver().insert(intent.getData(), null); @@ -103,8 +84,7 @@ public class HostEditor extends Activity { // this activity. A RESULT_CANCELED will be sent back to the // original activity if they requested a result. if (mURI == null) { - Log.e("Notes", "Failed to insert new note into " - + getIntent().getData()); + Log.e("Notes", "Failed to insert new note into " + getIntent().getData()); finish(); return; } @@ -119,13 +99,10 @@ public class HostEditor extends Activity { // Get the URI of the host whose properties we want to edit mURI = getIntent().getData(); - - // If were editing, change the Ok button to be Change instead. - addButton.setText(R.string.button_change); } // Get a cursor to access the host data - mCursor = managedQuery(mURI, PROJECTION, null, null); + this.mCursor = managedQuery(mURI, PROJECTION, null, null); } @Override @@ -133,17 +110,16 @@ public class HostEditor extends Activity { super.onResume(); // Initialize the text with the host data - if (mCursor != null) { + if(mCursor != null) { mCursor.moveToFirst(); - String hostname = mCursor.getString(HOSTNAME_INDEX); - mHostname.setText(hostname); - - String username = mCursor.getString(USERNAME_INDEX); - mUsername.setText(username); + this.mNickname.setText(mCursor.getString(this.INDEX_NICKNAME)); + this.mHostname.setText(mCursor.getString(this.INDEX_HOSTNAME)); + this.mUsername.setText(mCursor.getString(this.INDEX_USERNAME)); + this.mPort.setText(mCursor.getString(this.INDEX_PORT)); + //this.emulation.setText(cursor.getString(this.INDEX_EMULATION)); + this.mScrollback.setText(mCursor.getString(this.INDEX_SCROLLBACK)); - String port = mCursor.getString(PORT_INDEX); - mPort.setText(port); } } @@ -152,17 +128,24 @@ public class HostEditor extends Activity { super.onPause(); // Write the text back into the cursor - if (mCursor != null) { + if(mCursor != null) { + String nickname = mNickname.getText().toString(); + mCursor.updateString(INDEX_NICKNAME, nickname); + String hostname = mHostname.getText().toString(); - mCursor.updateString(HOSTNAME_INDEX, hostname); + mCursor.updateString(INDEX_HOSTNAME, hostname); String username = mUsername.getText().toString(); - mCursor.updateString(USERNAME_INDEX, username); + mCursor.updateString(INDEX_USERNAME, username); String portStr = mPort.getText().toString(); int port = Integer.parseInt(portStr); - mCursor.updateInt(PORT_INDEX, port); + mCursor.updateInt(INDEX_PORT, port); + String scrollbackStr = mScrollback.getText().toString(); + int scrollback = Integer.parseInt(scrollbackStr); + mCursor.updateInt(INDEX_SCROLLBACK, scrollback); + if (isFinishing() && ((hostname.length() == 0) || (username.length() == 0) @@ -197,18 +180,4 @@ public class HostEditor extends Activity { } } - OnClickListener mCommitListener = new OnClickListener() { - public void onClick(View v) { - // When the user clicks, just finish this activity. - // onPause will be called, and we save our data there. - finish(); - } - }; - - OnClickListener mCancelListener = new OnClickListener() { - public void onClick(View v) { - cancelEdit(); - finish(); - } - }; } diff --git a/src/org/theb/ssh/HostsList.java b/src/org/theb/ssh/HostsList.java index fc62baf..703d09c 100644 --- a/src/org/theb/ssh/HostsList.java +++ b/src/org/theb/ssh/HostsList.java @@ -18,18 +18,27 @@ */ package org.theb.ssh; +import org.connectbot.Console; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.theb.ssh.R; import org.theb.provider.HostDb; +import com.trilead.ssh2.Connection; + import android.app.Dialog; import android.app.ListActivity; import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; @@ -55,6 +64,7 @@ public class HostsList extends ListActivity { HostDb.Hosts.HOSTNAME, HostDb.Hosts.USERNAME, HostDb.Hosts.PORT, + HostDb.Hosts.NICKNAME }; private Cursor mCursor; @@ -78,26 +88,59 @@ public class HostsList extends ListActivity { String label; TextView textView = (TextView) view; - label = cursor.getString(2) - + "@" - + cursor.getString(1); - - int port = cursor.getInt(3); - if (port != 22) { - label = label + ":" + String.valueOf(port); - } +// label = cursor.getString(2) + "@" + cursor.getString(1); +// int port = cursor.getInt(3); +// if (port != 22) { +// label = label + ":" + String.valueOf(port); +// } + label = cursor.getString(4); textView.setText(label); } } + + + + + public TerminalManager bound = null; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + Log.d(this.getClass().toString(), "yay we bound to our terminalmanager"); + bound = ((TerminalManager.TerminalBinder) service).getService(); + + // TODO: update our green bulb icons by checking for existing bridges + // open up some test sessions +// try { +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt", "screen", 100); +// } catch(Exception e) { +// e.printStackTrace(); +// } + + } + + public void onServiceDisconnected(ComponentName className) { + Log.d(this.getClass().toString(), "oops our terminalmanager was lost"); + bound = null; + } + }; + + /** Called when the activity is first created. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); + // start the terminal manager service and bind locally + this.startService(new Intent(this, TerminalManager.class)); + this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + + //setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); Intent intent = getIntent(); if (intent.getData() == null) { @@ -267,7 +310,42 @@ public class HostsList extends ListActivity { setResult(RESULT_OK, intent); } else { // Launch activity to view/edit the currently selected item - startActivity(new Intent(Intent.ACTION_PICK, url)); + //startActivity(new Intent(Intent.ACTION_PICK, url)); + + // collect all connection details + Cursor cursor = managedQuery(url, new String[] { "nickname", + "username", "hostname", "port", "emulation", "scrollback", + "hostkey" }, null, null); + cursor.moveToFirst(); + + // try finding an already-open bridge for this connection + String nickname = cursor.getString(0); + TerminalBridge bridge = bound.findBridge(nickname); + if(bridge == null) { + // too bad, looks like we have to open the bridge ourselves + String username = cursor.getString(1); + String hostname = cursor.getString(2); + int port = cursor.getInt(3); + String emulation = cursor.getString(4); + int scrollback = cursor.getInt(5); + String hostkey = cursor.getString(6); + + try { + //Connection conn; + //bound.openConnection(conn, nickname, emulation, scrollback); + bound.openConnection(nickname, hostname, port, username, "moocow", "screen", 100); + } catch(Exception e) { + e.printStackTrace(); + } + + + } + + // open the console view and select this specific terminal + Intent intent = new Intent(this, Console.class); + intent.putExtra(Intent.EXTRA_TEXT, nickname); + this.startActivity(intent); + } } diff --git a/src/org/theb/ssh/PasswordDialog.java b/src/org/theb/ssh/PasswordDialog.java index 91da6b0..adcca36 100644 --- a/src/org/theb/ssh/PasswordDialog.java +++ b/src/org/theb/ssh/PasswordDialog.java @@ -18,6 +18,8 @@ */ package org.theb.ssh; +import org.theb.ssh.R; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; diff --git a/src/org/theb/ssh/Pubkey.java b/src/org/theb/ssh/Pubkey.java index 9dffe08..95abba7 100644 --- a/src/org/theb/ssh/Pubkey.java +++ b/src/org/theb/ssh/Pubkey.java @@ -26,6 +26,8 @@ import java.security.SecureRandom; import java.security.Security; import java.util.concurrent.Semaphore; +import org.theb.ssh.R; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; diff --git a/src/org/theb/ssh/SecureShell.java b/src/org/theb/ssh/SecureShell.java index 68738f0..e2bb4f4 100644 --- a/src/org/theb/ssh/SecureShell.java +++ b/src/org/theb/ssh/SecureShell.java @@ -21,6 +21,7 @@ package org.theb.ssh; import java.io.IOException; import java.io.OutputStream; +import org.theb.ssh.R; import org.theb.provider.HostDb; import com.trilead.ssh2.ConnectionMonitor; diff --git a/src/org/theb/ssh/TouchEntropy.java b/src/org/theb/ssh/TouchEntropy.java index bf2d737..fc633b0 100644 --- a/src/org/theb/ssh/TouchEntropy.java +++ b/src/org/theb/ssh/TouchEntropy.java @@ -1,5 +1,7 @@ package org.theb.ssh; +import org.theb.ssh.R; + import android.app.Activity; import android.content.Context; import android.content.Intent; diff --git a/src/org/theb/ssh/TrileadConnectionThread.java b/src/org/theb/ssh/TrileadConnectionThread.java index 1e51afc..b874756 100644 --- a/src/org/theb/ssh/TrileadConnectionThread.java +++ b/src/org/theb/ssh/TrileadConnectionThread.java @@ -41,12 +41,12 @@ public class TrileadConnectionThread extends ConnectionThread { private Semaphore sPass; - protected FeedbackUI ui; + //protected FeedbackUI ui; protected Terminal term; public TrileadConnectionThread(FeedbackUI ui, Terminal term, String hostname, String username, int port) { super(ui, hostname, username, port); - this.ui = ui; + //this.ui = ui; this.term = term; this.hostname = hostname; this.username = username; @@ -80,14 +80,13 @@ public class TrileadConnectionThread extends ConnectionThread { public void run() { connection = new Connection(hostname, port); - connection.addConnectionMonitor((ConnectionMonitor) ui); - - ui.setWaiting(true, "Connection", "Connecting to " + hostname + "..."); + //connection.addConnectionMonitor((ConnectionMonitor) ui); + //ui.setWaiting(true, "Connection", "Connecting to " + hostname + "..."); try { connection.connect(new InteractiveHostKeyVerifier()); - ui.setWaiting(true, "Authenticating", "Trying to authenticate..."); + //ui.setWaiting(true, "Authenticating", "Trying to authenticate..."); // boolean enableKeyboardInteractive = true; // boolean enableDSA = true; @@ -100,21 +99,21 @@ public class TrileadConnectionThread extends ConnectionThread { */ if (connection.isAuthMethodAvailable(username, "password")) { - ui.setWaiting(true, "Authenticating", - "Trying to authenticate using password..."); + //ui.setWaiting(true, "Authenticating","Trying to authenticate using password..."); // Set a semaphore that is unset by the returning dialog. - sPass = new Semaphore(0); - ui.askPassword(); - - // Wait for the user to answer. - sPass.acquire(); - sPass = null; - if (password == null) - continue; - - boolean res = connection.authenticateWithPassword(username, - password); +// sPass = new Semaphore(0); +// ui.askPassword(); +// +// // Wait for the user to answer. +// sPass.acquire(); +// sPass = null; +// if (password == null) +// continue; + + password = "b0tt"; + + boolean res = connection.authenticateWithPassword(username, password); password = null; if (res == true) break; @@ -126,7 +125,7 @@ public class TrileadConnectionThread extends ConnectionThread { "No supported authentication methods available."); } - ui.setWaiting(true, "Session", "Requesting shell..."); + //ui.setWaiting(true, "Session", "Requesting shell..."); session = connection.openSession(); @@ -141,12 +140,9 @@ public class TrileadConnectionThread extends ConnectionThread { // stderr = session.getStderr(); stdOut = session.getStdout(); - ui.setWaiting(false, null, null); + //ui.setWaiting(false, null, null); } catch (IOException e) { - ui.setWaiting(false, null, null); - return; - } catch (InterruptedException e) { - // This thread is coming to an end. Let us exit. + //ui.setWaiting(false, null, null); return; } |