aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/de/mud/terminal/vt320.java20
-rw-r--r--app/src/main/java/org/connectbot/ConsoleActivity.java355
-rw-r--r--app/src/main/java/org/connectbot/EditHostActivity.java217
-rw-r--r--app/src/main/java/org/connectbot/HostEditorFragment.java146
-rw-r--r--app/src/main/java/org/connectbot/HostListActivity.java8
-rw-r--r--app/src/main/java/org/connectbot/TerminalView.java483
-rw-r--r--app/src/main/java/org/connectbot/bean/HostBean.java6
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalBridge.java43
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalKeyListener.java8
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalManager.java2
-rw-r--r--app/src/main/java/org/connectbot/transport/SSH.java2
-rw-r--r--app/src/main/java/org/connectbot/util/HostDatabase.java2
-rw-r--r--app/src/main/java/org/connectbot/util/TerminalViewPager.java61
-rw-r--r--app/src/main/res/layout-large/act_console.xml2
-rw-r--r--app/src/main/res/layout/act_console.xml2
-rw-r--r--app/src/main/res/menu/edit_host_activity_add_menu.xml30
-rw-r--r--app/src/main/res/menu/edit_host_activity_edit_menu.xml30
-rw-r--r--app/src/main/res/values/strings.xml4
-rw-r--r--app/src/main/res/xml/preferences.xml2
-rw-r--r--config/quality.gradle5
20 files changed, 1022 insertions, 406 deletions
diff --git a/app/src/main/java/de/mud/terminal/vt320.java b/app/src/main/java/de/mud/terminal/vt320.java
index dc95bea..3c929b2 100644
--- a/app/src/main/java/de/mud/terminal/vt320.java
+++ b/app/src/main/java/de/mud/terminal/vt320.java
@@ -672,6 +672,7 @@ public void setScreenSize(int c, int r, boolean broadcast) {
boolean capslock = false;
boolean numlock = false;
int mouserpt = 0;
+ int mouserptSaved = 0;
byte mousebut = 0;
boolean useibmcharset = false;
@@ -2197,9 +2198,20 @@ public void setScreenSize(int c, int r, boolean broadcast) {
DCEvars[DCEvar] = 0;
term_state = TSTATE_DCEQ;
break;
- case 's': // XTERM_SAVE missing!
- if (true || debug > 1)
- debug("ESC [ ? " + DCEvars[0] + " s unimplemented!");
+ case 's':
+ for (int i = 0; i <= DCEvar; i++) {
+ switch (DCEvars[i]) {
+ case 9:
+ case 1000:
+ case 1001:
+ case 1002:
+ case 1003:
+ mouserptSaved = mouserpt;
+ break;
+ default:
+ debug("ESC [ ? " + DCEvars[0] + " s, unimplemented!");
+ }
+ }
break;
case 'r': // XTERM_RESTORE
if (true || debug > 1)
@@ -2227,7 +2239,7 @@ public void setScreenSize(int c, int r, boolean broadcast) {
case 1001:
case 1002:
case 1003:
- mouserpt = DCEvars[i];
+ mouserpt = mouserptSaved;
break;
default:
debug("ESC [ ? " + DCEvars[0] + " r, unimplemented!");
diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java
index d628a07..440661a 100644
--- a/app/src/main/java/org/connectbot/ConsoleActivity.java
+++ b/app/src/main/java/org/connectbot/ConsoleActivity.java
@@ -22,15 +22,14 @@ import java.util.ArrayList;
import java.util.List;
import org.connectbot.bean.HostBean;
-import org.connectbot.bean.SelectionArea;
import org.connectbot.service.BridgeDisconnectedListener;
import org.connectbot.service.PromptHelper;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalKeyListener;
import org.connectbot.service.TerminalManager;
import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.TerminalViewPager;
-import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ComponentName;
@@ -50,20 +49,15 @@ import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
-import android.support.v4.app.ActivityCompat;
import android.support.design.widget.TabLayout;
+import android.support.v4.app.ActivityCompat;
import android.support.v4.view.MenuItemCompat;
-import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.ClipboardManager;
import android.util.Log;
-import android.view.ContextMenu;
-import android.view.GestureDetector;
-import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -74,7 +68,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
-import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
@@ -100,14 +93,12 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
protected static final int REQUEST_EDIT = 1;
- private static final int CLICK_TIME = 400;
- private static final float MAX_CLICK_DISTANCE = 25f;
private static final int KEYBOARD_DISPLAY_TIME = 3000;
private static final int KEYBOARD_REPEAT_INITIAL = 500;
private static final int KEYBOARD_REPEAT = 100;
private static final String STATE_SELECTED_URI = "selectedUri";
- protected ViewPager pager = null;
+ protected TerminalViewPager pager = null;
protected TabLayout tabs = null;
protected Toolbar toolbar = null;
@Nullable
@@ -140,15 +131,11 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
private Animation fade_out_delayed;
private Animation keyboard_fade_in, keyboard_fade_out;
- private float lastX, lastY;
private InputMethodManager inputManager;
private MenuItem disconnect, copy, paste, portForward, resize, urlscan;
- protected TerminalBridge copySource = null;
- private int lastTouchRow, lastTouchCol;
-
private boolean forcedOrientation;
private Handler handler = new Handler();
@@ -498,10 +485,11 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
inflater = LayoutInflater.from(this);
toolbar = (Toolbar) findViewById(R.id.toolbar);
- pager = (ViewPager) findViewById(R.id.console_flip);
- registerForContextMenu(pager);
+
+ pager = (TerminalViewPager) findViewById(R.id.console_flip);
+
pager.addOnPageChangeListener(
- new ViewPager.SimpleOnPageChangeListener() {
+ new TerminalViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
setTitle(adapter.getPageTitle(position));
@@ -669,258 +657,17 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
if (tabs != null)
setupTabLayoutWithViewPager();
- // detect fling gestures to switch between terminals
- final GestureDetector detect = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
- private float totalY = 0;
-
+ pager.setOnClickListener(new OnClickListener() {
@Override
- public void onLongPress(MotionEvent e) {
- super.onLongPress(e);
- openContextMenu(pager);
- }
-
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
-
- // if copying, then ignore
- if (copySource != null && copySource.isSelectingForCopy())
- return false;
-
- if (e1 == null || e2 == null)
- return false;
-
- // if releasing then reset total scroll
- if (e2.getAction() == MotionEvent.ACTION_UP) {
- totalY = 0;
- }
-
- // activate consider if within x tolerance
- int touchSlop = ViewConfiguration.get(ConsoleActivity.this).getScaledTouchSlop();
- if (Math.abs(e1.getX() - e2.getX()) < touchSlop * 4) {
-
- View view = adapter.getCurrentTerminalView();
- if (view == null) return false;
- TerminalView terminal = (TerminalView) view;
-
- // estimate how many rows we have scrolled through
- // accumulate distance that doesn't trigger immediate scroll
- totalY += distanceY;
- final int moved = (int) (totalY / terminal.bridge.charHeight);
-
- // consume as scrollback only if towards right half of screen
- if (e2.getX() > view.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) {
- ((vt320) terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
- terminal.bridge.tryKeyVibrate();
- totalY = 0;
- return true;
- } else if (moved < -5) {
- ((vt320) terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
- terminal.bridge.tryKeyVibrate();
- totalY = 0;
- return true;
- }
-
- }
-
- }
-
- return false;
- }
-
-
- });
-
- pager.setLongClickable(true);
- pager.setOnTouchListener(new OnTouchListener() {
-
- public boolean onTouch(View v, MotionEvent event) {
- TerminalBridge bridge = adapter.getCurrentTerminalView().bridge;
-
- // Handle mouse-specific actions.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
- MotionEventCompat.getSource(event) == InputDevice.SOURCE_MOUSE) {
- if (onMouseEvent(event, bridge)) {
- return true;
- }
- }
-
- // when copying, highlight the area
- if (copySource != null && copySource.isSelectingForCopy()) {
- SelectionArea area = copySource.getSelectionArea();
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // recording starting area
- if (area.isSelectingOrigin()) {
- area.setRow(row);
- area.setColumn(col);
- lastTouchRow = row;
- lastTouchCol = col;
- copySource.redraw();
- }
- return true;
- case MotionEvent.ACTION_MOVE:
- /* ignore when user hasn't moved since last time so
- * we can fine-tune with directional pad
- */
- if (row == lastTouchRow && col == lastTouchCol)
- return true;
-
- // if the user moves, start the selection for other corner
- area.finishSelectingOrigin();
-
- // update selected area
- area.setRow(row);
- area.setColumn(col);
- lastTouchRow = row;
- lastTouchCol = col;
- copySource.redraw();
- return true;
- case MotionEvent.ACTION_UP:
- /* If they didn't move their finger, maybe they meant to
- * select the rest of the text with the directional pad.
- */
- if (area.getLeft() == area.getRight() &&
- area.getTop() == area.getBottom()) {
- return true;
- }
-
- // copy selected area to clipboard
- String copiedText = area.copyFrom(copySource.buffer);
-
- clipboard.setText(copiedText);
- Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show();
- // fall through to clear state
-
- case MotionEvent.ACTION_CANCEL:
- // make sure we clear any highlighted area
- area.reset();
- copySource.setSelectingForCopy(false);
- copySource.redraw();
- return true;
- }
- }
-
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- lastX = event.getX();
- lastY = event.getY();
- } else if (event.getAction() == MotionEvent.ACTION_UP
- && keyboardGroup.getVisibility() == View.GONE
- && event.getEventTime() - event.getDownTime() < CLICK_TIME
- && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
- && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
- showEmulatedKeys(true);
- }
-
- // pass any touch events back to detector
- return detect.onTouchEvent(event);
- }
-
- /**
- * @param event
- * @param bridge
- * @return True if the event is handled.
- */
- @TargetApi(14)
- private boolean onMouseEvent(MotionEvent event, TerminalBridge bridge) {
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
- int meta = event.getMetaState();
- boolean shiftOn = (meta & KeyEvent.META_SHIFT_ON) != 0;
- boolean mouseReport = ((vt320) bridge.buffer).isMouseReportEnabled();
-
- // MouseReport can be "defeated" using the shift key.
- if ((!mouseReport || shiftOn)) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- switch (event.getButtonState()) {
- case MotionEvent.BUTTON_PRIMARY:
- // Automatically start copy mode if using a mouse.
- startCopyMode();
- break;
- case MotionEvent.BUTTON_SECONDARY:
- openContextMenu(pager);
- return true;
- case MotionEvent.BUTTON_TERTIARY:
- // Middle click pastes.
- pasteIntoTerminal();
- return true;
- }
- }
- } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
- ((vt320) bridge.buffer).mousePressed(
- col, row, mouseEventToJavaModifiers(event));
- return true;
- } else if (event.getAction() == MotionEvent.ACTION_UP) {
- ((vt320) bridge.buffer).mouseReleased(col, row);
- return true;
- } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
- int buttonState = event.getButtonState();
- int button = (buttonState & MotionEvent.BUTTON_PRIMARY) != 0 ? 0 :
- (buttonState & MotionEvent.BUTTON_SECONDARY) != 0 ? 1 :
- (buttonState & MotionEvent.BUTTON_TERTIARY) != 0 ? 2 : 3;
- ((vt320) bridge.buffer).mouseMoved(
- button,
- col,
- row,
- (meta & KeyEvent.META_CTRL_ON) != 0,
- (meta & KeyEvent.META_SHIFT_ON) != 0,
- (meta & KeyEvent.META_META_ON) != 0);
- return true;
+ public void onClick(View v) {
+ if (keyboardGroup.getVisibility() == View.GONE) {
+ showEmulatedKeys(false);
}
-
- return false;
}
-
});
}
/**
- * Takes an android mouse event and produces a Java InputEvent modifiers int which can be
- * passed to vt320.
- * @param mouseEvent The {@link MotionEvent} which should be a mouse click or release.
- * @return A Java InputEvent modifier int. See
- * http://docs.oracle.com/javase/7/docs/api/java/awt/event/InputEvent.html
- */
- @TargetApi(14)
- private static int mouseEventToJavaModifiers(MotionEvent mouseEvent) {
- if (MotionEventCompat.getSource(mouseEvent) != InputDevice.SOURCE_MOUSE) return 0;
-
- int mods = 0;
-
- // See http://docs.oracle.com/javase/7/docs/api/constant-values.html
- int buttonState = mouseEvent.getButtonState();
- if ((buttonState & MotionEvent.BUTTON_PRIMARY) != 0)
- mods |= 16;
- if ((buttonState & MotionEvent.BUTTON_SECONDARY) != 0)
- mods |= 8;
- if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0)
- mods |= 4;
-
- // Note: Meta and Ctrl are intentionally swapped here to keep logic in vt320 simple.
- int meta = mouseEvent.getMetaState();
- if ((meta & KeyEvent.META_META_ON) != 0)
- mods |= 2;
- if ((meta & KeyEvent.META_SHIFT_ON) != 0)
- mods |= 1;
- if ((meta & KeyEvent.META_CTRL_ON) != 0)
- mods |= 4;
-
- return mods;
- }
-
- /**
* Ties the {@link TabLayout} to the {@link ViewPager}.
*
* <p>This method will:
@@ -1011,19 +758,21 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
}
});
- copy = menu.add(R.string.console_menu_copy);
- if (hardKeyboard)
- copy.setAlphabeticShortcut('c');
- MenuItemCompat.setShowAsAction(copy, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
- copy.setIcon(R.drawable.ic_action_copy);
- copy.setEnabled(activeTerminal);
- copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- public boolean onMenuItemClick(MenuItem item) {
- startCopyMode();
- Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
- return true;
- }
- });
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ copy = menu.add(R.string.console_menu_copy);
+ if (hardKeyboard)
+ copy.setAlphabeticShortcut('c');
+ MenuItemCompat.setShowAsAction(copy, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ copy.setIcon(R.drawable.ic_action_copy);
+ copy.setEnabled(activeTerminal);
+ copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ adapter.getCurrentTerminalView().startPreHoneycombCopyMode();
+ Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
+ return true;
+ }
+ });
+ }
paste = menu.add(R.string.console_menu_paste);
if (hardKeyboard)
@@ -1144,7 +893,10 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
disconnect.setTitle(R.string.list_host_disconnect);
else
disconnect.setTitle(R.string.console_menu_close);
- copy.setEnabled(activeTerminal);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ copy.setEnabled(activeTerminal);
+ }
paste.setEnabled(clipboard.hasText() && sessionOpen);
portForward.setEnabled(sessionOpen && canForwardPorts);
urlscan.setEnabled(activeTerminal);
@@ -1174,32 +926,6 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- final TerminalView view = adapter.getCurrentTerminalView();
- boolean activeTerminal = view != null;
- boolean sessionOpen = false;
-
- if (activeTerminal) {
- TerminalBridge bridge = view.bridge;
- sessionOpen = bridge.isSessionOpen();
- }
-
- MenuItem paste = menu.add(R.string.console_menu_paste);
- if (hardKeyboard)
- paste.setAlphabeticShortcut('v');
- paste.setIcon(android.R.drawable.ic_menu_edit);
- paste.setEnabled(clipboard.hasText() && sessionOpen);
- paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- public boolean onMenuItemClick(MenuItem item) {
- pasteIntoTerminal();
- return true;
- }
- });
-
-
- }
-
- @Override
public void onStart() {
super.onStart();
@@ -1308,21 +1034,6 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
super.onSaveInstanceState(savedInstanceState);
}
- private void startCopyMode() {
- // mark as copying and reset any previous bounds
- TerminalView terminalView = (TerminalView) adapter.getCurrentTerminalView();
- copySource = terminalView.bridge;
-
- SelectionArea area = copySource.getSelectionArea();
- area.reset();
- area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows());
-
- copySource.setSelectingForCopy(true);
-
- // Make sure we show the initial selection
- copySource.redraw();
- }
-
/**
* Save the currently shown {@link TerminalView} as the default. This is
* saved back down into {@link TerminalManager} where we can read it again
@@ -1494,7 +1205,7 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
overlay.setText(bridge.host.getNickname());
// and add our terminal view control, using index to place behind overlay
- final TerminalView terminal = new TerminalView(container.getContext(), bridge);
+ final TerminalView terminal = new TerminalView(container.getContext(), bridge, pager);
terminal.setId(R.id.terminal_view);
view.addView(terminal, 0);
@@ -1572,7 +1283,9 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
public TerminalView getCurrentTerminalView() {
View currentView = pager.findViewWithTag(getBridgeAtPosition(pager.getCurrentItem()));
- if (currentView == null) return null;
+ if (currentView == null) {
+ return null;
+ }
return (TerminalView) currentView.findViewById(R.id.terminal_view);
}
}
diff --git a/app/src/main/java/org/connectbot/EditHostActivity.java b/app/src/main/java/org/connectbot/EditHostActivity.java
index 6c7da11..f61924f 100644
--- a/app/src/main/java/org/connectbot/EditHostActivity.java
+++ b/app/src/main/java/org/connectbot/EditHostActivity.java
@@ -17,27 +17,236 @@
package org.connectbot;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.TypedArray;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PubkeyDatabase;
public class EditHostActivity extends AppCompatActivity implements HostEditorFragment.Listener {
+ private static final String EXTRA_EXISTING_HOST_ID = "org.connectbot.existing_host_id";
+ private static final long NO_HOST_ID = -1;
+
+ private HostDatabase mHostDb;
+ private PubkeyDatabase mPubkeyDb;
+ private ServiceConnection mTerminalConnection;
+ private HostBean mHost;
+ private TerminalBridge mBridge;
+ private boolean mIsCreating;
+ private MenuItem mSaveHostButton;
+
+ public static Intent createIntentForExistingHost(Context context, long existingHostId) {
+ Intent i = new Intent(context, EditHostActivity.class);
+ i.putExtra(EXTRA_EXISTING_HOST_ID, existingHostId);
+ return i;
+ }
+
+ public static Intent createIntentForNewHost(Context context) {
+ return createIntentForExistingHost(context, NO_HOST_ID);
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ mHostDb = HostDatabase.get(this);
+ mPubkeyDb = PubkeyDatabase.get(this);
+
+ mTerminalConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ TerminalManager bound = ((TerminalManager.TerminalBinder) service).getService();
+ mBridge = bound.getConnectedBridge(mHost);
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ mBridge = null;
+ }
+ };
+
+ long hostId = getIntent().getLongExtra(EXTRA_EXISTING_HOST_ID, NO_HOST_ID);
+ mIsCreating = hostId == NO_HOST_ID;
+ mHost = mIsCreating ? null : mHostDb.findHostById(hostId);
+
+ // Note that the lists must be explicitly declared as ArrayLists because Bundle only accepts
+ // ArrayLists of Strings.
+ ArrayList<String> pubkeyNames = new ArrayList<>();
+ ArrayList<String> pubkeyValues = new ArrayList<>();
+
+ // First, add default pubkey names and values (e.g., "use any" and "don't use any").
+ TypedArray defaultPubkeyNames = getResources().obtainTypedArray(R.array.list_pubkeyids);
+ for (int i = 0; i < defaultPubkeyNames.length(); i++) {
+ pubkeyNames.add(defaultPubkeyNames.getString(i));
+ }
+ TypedArray defaultPubkeyValues = getResources().obtainTypedArray(R.array.list_pubkeyids_value);
+ for (int i = 0; i < defaultPubkeyValues.length(); i++) {
+ pubkeyValues.add(defaultPubkeyValues.getString(i));
+ }
+
+ // Now, add pubkeys which have been added by the user.
+ for (CharSequence cs : mPubkeyDb.allValues(PubkeyDatabase.FIELD_PUBKEY_NICKNAME)) {
+ pubkeyNames.add(cs.toString());
+ }
+ for (CharSequence cs : mPubkeyDb.allValues("_id")) {
+ pubkeyValues.add(cs.toString());
+ }
+
setContentView(R.layout.activity_edit_host);
+ FragmentManager fm = getSupportFragmentManager();
+ HostEditorFragment fragment =
+ (HostEditorFragment) fm.findFragmentById(R.id.fragment_container);
- if (savedInstanceState == null) {
- HostEditorFragment editor = HostEditorFragment.newInstance(null);
+ if (fragment == null) {
+ fragment = HostEditorFragment.newInstance(mHost, pubkeyNames, pubkeyValues);
getSupportFragmentManager().beginTransaction()
- .add(R.id.fragment_container, editor).commit();
+ .add(R.id.fragment_container, fragment).commit();
}
+
+ defaultPubkeyNames.recycle();
+ defaultPubkeyValues.recycle();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(
+ mIsCreating ? R.menu.edit_host_activity_add_menu : R.menu.edit_host_activity_edit_menu,
+ menu);
+
+ mSaveHostButton = menu.getItem(0);
+
+ // If the new host is being created, it can't be added until modifications have been made.
+ mSaveHostButton.setEnabled(!mIsCreating);
+
+ return super.onCreateOptionsMenu(menu);
}
@Override
- public void onHostUpdated(HostBean host) {
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.save:
+ mHostDb.saveHost(mHost);
+
+ if (mBridge != null) {
+ // If the console is already open, apply the new encoding now. If the console
+ // was not yet opened, this will be applied automatically when it is opened.
+ mBridge.setCharset(mHost.getEncoding());
+ }
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ bindService(new Intent(
+ this, TerminalManager.class), mTerminalConnection, Context.BIND_AUTO_CREATE);
+
+ final HostEditorFragment fragment = (HostEditorFragment) getSupportFragmentManager().
+ findFragmentById(R.id.fragment_container);
+ if (CharsetHolder.isInitialized()) {
+ fragment.setCharsetData(CharsetHolder.getCharsetData());
+ } else {
+ // If CharsetHolder is uninitialized, initialize it in an AsyncTask. This is necessary
+ // because Charset must touch the disk, which cannot be performed on the UI thread.
+ AsyncTask<Void, Void, Void> charsetTask = new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(Void... unused) {
+ CharsetHolder.initialize();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void unused) {
+ fragment.setCharsetData(CharsetHolder.getCharsetData());
+ }
+ };
+ charsetTask.execute();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ unbindService(mTerminalConnection);
+ }
+
+ @Override
+ public void onValidHostConfigured(HostBean host) {
+ mHost = host;
+ if (mSaveHostButton != null)
+ mSaveHostButton.setEnabled(true);
+ }
+
+ @Override
+ public void onHostInvalidated() {
+ mHost = null;
+ if (mSaveHostButton != null)
+ mSaveHostButton.setEnabled(false);
+ }
+
+ // Private static class used to generate a list of available Charsets. Note that this class
+ // must not be initialized by the UI thread because it blocks on disk access.
+ private static class CharsetHolder {
+ private static boolean mInitialized = false;
+
+ // Map from Charset display name to Charset value (i.e., unique ID).
+ private static Map<String, String> mData;
+
+ public static Map<String, String> getCharsetData() {
+ if (mData == null)
+ initialize();
+
+ return mData;
+ }
+
+ private synchronized static void initialize() {
+ if (mInitialized)
+ return;
+
+ mData = new HashMap<>();
+ for (Map.Entry<String, Charset> entry : Charset.availableCharsets().entrySet()) {
+ Charset c = entry.getValue();
+ if (c.canEncode() && c.isRegistered()) {
+ String key = entry.getKey();
+ if (key.startsWith("cp")) {
+ // Custom CP437 charset changes.
+ mData.put("CP437", "CP437");
+ }
+ mData.put(c.displayName(), entry.getKey());
+ }
+ }
+
+ mInitialized = true;
+ }
+
+ public static boolean isInitialized() {
+ return mInitialized;
+ }
}
}
diff --git a/app/src/main/java/org/connectbot/HostEditorFragment.java b/app/src/main/java/org/connectbot/HostEditorFragment.java
index 6646b4a..56022d7 100644
--- a/app/src/main/java/org/connectbot/HostEditorFragment.java
+++ b/app/src/main/java/org/connectbot/HostEditorFragment.java
@@ -17,6 +17,9 @@
package org.connectbot;
+import java.util.ArrayList;
+import java.util.Map;
+
import android.content.ContentValues;
import android.content.Context;
import android.content.res.TypedArray;
@@ -49,14 +52,22 @@ import org.connectbot.util.HostDatabase;
public class HostEditorFragment extends Fragment {
+ private static final String ARG_EXISTING_HOST_ID = "existingHostId";
private static final String ARG_EXISTING_HOST = "existingHost";
private static final String ARG_IS_EXPANDED = "isExpanded";
+ private static final String ARG_PUBKEY_NAMES = "pubkeyNames";
+ private static final String ARG_PUBKEY_VALUES = "pubkeyValues";
private static final String ARG_QUICKCONNECT_STRING = "quickConnectString";
private static final int MINIMUM_FONT_SIZE = 8;
// The host being edited.
private HostBean mHost;
+
+ // The pubkey lists (names and values). Note that these are declared as ArrayLists rather than
+ // Lists because Bundles can only contain ArrayLists, not general Lists.
+ private ArrayList<String> mPubkeyNames;
+ private ArrayList<String> mPubkeyValues;
// Whether the host is being created for the first time (as opposed to an existing one being
// edited).
@@ -112,12 +123,16 @@ public class HostEditorFragment extends Fragment {
private Spinner mDelKeySpinner;
private Spinner mEncodingSpinner;
- public static HostEditorFragment newInstance(HostBean existingHost) {
+ public static HostEditorFragment newInstance(
+ HostBean existingHost, ArrayList<String> pubkeyNames, ArrayList<String> pubkeyValues) {
HostEditorFragment fragment = new HostEditorFragment();
Bundle args = new Bundle();
if (existingHost != null) {
+ args.putLong(ARG_EXISTING_HOST_ID, existingHost.getId());
args.putParcelable(ARG_EXISTING_HOST, existingHost.getValues());
}
+ args.putStringArrayList(ARG_PUBKEY_NAMES, pubkeyNames);
+ args.putStringArrayList(ARG_PUBKEY_VALUES, pubkeyValues);
fragment.setArguments(args);
return fragment;
}
@@ -135,10 +150,14 @@ public class HostEditorFragment extends Fragment {
mIsCreating = existingHostParcelable == null;
if (existingHostParcelable != null) {
mHost = HostBean.fromContentValues((ContentValues) existingHostParcelable);
+ mHost.setId(bundle.getLong(ARG_EXISTING_HOST_ID));
} else {
mHost = new HostBean();
}
+ mPubkeyNames = bundle.getStringArrayList(ARG_PUBKEY_NAMES);
+ mPubkeyValues = bundle.getStringArrayList(ARG_PUBKEY_VALUES);
+
mIsUriEditorExpanded = bundle.getBoolean(ARG_IS_EXPANDED);
}
@@ -154,7 +173,7 @@ public class HostEditorFragment extends Fragment {
transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mTransportSpinner.setAdapter(transportSelection);
for (int i = 0; i < transportNames.length; i++) {
- if (transportNames.equals(mHost.getProtocol())) {
+ if (transportNames[i].equals(mHost.getProtocol())) {
mTransportSpinner.setSelection(i);
break;
}
@@ -171,6 +190,7 @@ public class HostEditorFragment extends Fragment {
mHost.setProtocol(protocol);
mHost.setPort(TransportFactory.getTransport(protocol).getDefaultPort());
+ handleHostChange();
mQuickConnectContainer.setHint(
TransportFactory.getFormatHint(protocol, getActivity()));
@@ -208,10 +228,12 @@ public class HostEditorFragment extends Fragment {
mQuickConnectField.setText(oldQuickConnect == null ? mHost.toString() : oldQuickConnect);
mQuickConnectField.addTextChangedListener(new TextWatcher() {
@Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
@Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
@Override
public void afterTextChanged(Editable s) {
@@ -265,8 +287,8 @@ public class HostEditorFragment extends Fragment {
new HostTextFieldWatcher(HostDatabase.FIELD_HOST_NICKNAME));
mColorSelector = (Spinner) view.findViewById(R.id.color_selector);
- for (int i = 0; i < mColorValues.getIndexCount(); i++) {
- if (mHost.getColor().equals(mColorValues.getString(i))) {
+ for (int i = 0; i < mColorValues.length(); i++) {
+ if (mColorValues.getString(i).equals(mHost.getColor())) {
mColorSelector.setSelection(i);
break;
}
@@ -275,6 +297,7 @@ public class HostEditorFragment extends Fragment {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
mHost.setColor(mColorValues.getString(position));
+ handleHostChange();
}
@Override
@@ -289,6 +312,7 @@ public class HostEditorFragment extends Fragment {
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int fontSize = MINIMUM_FONT_SIZE + progress;
mHost.setFontSize(fontSize);
+ handleHostChange();
mFontSizeText.setText(Integer.toString(fontSize));
}
@@ -303,8 +327,30 @@ public class HostEditorFragment extends Fragment {
mFontSizeSeekBar.setProgress(mHost.getFontSize() - MINIMUM_FONT_SIZE);
mPubkeySpinner = (Spinner) view.findViewById(R.id.pubkey_spinner);
- // TODO: Set up spinner. This requires passing pubkey data into the fragment from the
- // activity and will be part of an upcoming PR.
+ final String[] pubkeyNames = new String[mPubkeyNames.size()];
+ mPubkeyNames.toArray(pubkeyNames);
+ ArrayAdapter<String> pubkeySelection = new ArrayAdapter<String>(
+ getActivity(), android.R.layout.simple_spinner_item, pubkeyNames);
+ pubkeySelection.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mPubkeySpinner.setAdapter(pubkeySelection);
+ for (int i = 0; i < pubkeyNames.length; i++) {
+ if (mHost.getPubkeyId() == Integer.parseInt(mPubkeyValues.get(i))) {
+ mPubkeySpinner.setSelection(i);
+ break;
+ }
+ }
+ mPubkeySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ mHost.setPubkeyId(Integer.parseInt(mPubkeyValues.get(position)));
+ handleHostChange();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
mUseSshConfirmationContainer = view.findViewById(R.id.ssh_confirmation_container);
mUseSshAuthSwitch = (SwitchCompat) view.findViewById(R.id.use_ssh_auth_switch);
@@ -323,19 +369,21 @@ public class HostEditorFragment extends Fragment {
} else {
mHost.setUseAuthAgent(/* don't use */ mSshAuthValues.getString(0));
}
+ handleHostChange();
}
};
- mUseSshAuthSwitch.setOnCheckedChangeListener(authSwitchListener);
- mSshAuthConfirmationCheckbox.setOnCheckedChangeListener(authSwitchListener);
if (mHost.getUseAuthAgent() == null ||
mHost.getUseAuthAgent().equals(mSshAuthValues.getString(0))) {
mUseSshAuthSwitch.setChecked(false);
mSshAuthConfirmationCheckbox.setChecked(false);
} else {
mUseSshAuthSwitch.setChecked(true);
+ mUseSshConfirmationContainer.setVisibility(View.VISIBLE);
mSshAuthConfirmationCheckbox.setChecked(
mHost.getUseAuthAgent().equals(mSshAuthValues.getString(1)));
}
+ mUseSshAuthSwitch.setOnCheckedChangeListener(authSwitchListener);
+ mSshAuthConfirmationCheckbox.setOnCheckedChangeListener(authSwitchListener);
mCompressionSwitch = (SwitchCompat) view.findViewById(R.id.compression_switch);
mCompressionSwitch.setChecked(mHost.getCompression());
@@ -363,7 +411,7 @@ public class HostEditorFragment extends Fragment {
new HostTextFieldWatcher(HostDatabase.FIELD_HOST_POSTLOGIN));
mDelKeySpinner = (Spinner) view.findViewById(R.id.del_key_spinner);
- for (int i = 0; i < mDelKeyValues.getIndexCount(); i++) {
+ for (int i = 0; i < mDelKeyValues.length(); i++) {
if (mHost.getDelKey().equals(mDelKeyValues.getString(i))) {
mDelKeySpinner.setSelection(i);
break;
@@ -373,6 +421,7 @@ public class HostEditorFragment extends Fragment {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
mHost.setDelKey(mDelKeyValues.getString(position));
+ handleHostChange();
}
@Override
@@ -381,8 +430,8 @@ public class HostEditorFragment extends Fragment {
});
mEncodingSpinner = (Spinner) view.findViewById(R.id.encoding_spinner);
- // TODO: Set up spinner. This requires passing pubkey data into the fragment from the
- // activity and will be part of an upcoming PR.
+ // The spinner is initialized in setCharsetData() because Charset data is not always
+ // available when this fragment is created.
setUriPartsContainerExpanded(mIsUriEditorExpanded);
@@ -418,10 +467,48 @@ public class HostEditorFragment extends Fragment {
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
+ savedInstanceState.putLong(ARG_EXISTING_HOST_ID, mHost.getId());
savedInstanceState.putParcelable(ARG_EXISTING_HOST, mHost.getValues());
savedInstanceState.putBoolean(ARG_IS_EXPANDED, mIsUriEditorExpanded);
savedInstanceState.putString(
ARG_QUICKCONNECT_STRING, mQuickConnectField.getText().toString());
+ savedInstanceState.putStringArrayList(ARG_PUBKEY_NAMES, mPubkeyNames);
+ savedInstanceState.putStringArrayList(ARG_PUBKEY_VALUES, mPubkeyValues);
+ }
+
+ /**
+ * Sets the Charset encoding data for the editor.
+ * @param data A map from Charset display name to Charset value (i.e., unique ID for the
+ * Charset).
+ */
+ public void setCharsetData(final Map<String, String> data) {
+ if (mEncodingSpinner != null) {
+ final String[] encodingNames = new String[data.keySet().size()];
+ data.keySet().toArray(encodingNames);
+ ArrayAdapter<String> encodingSelection = new ArrayAdapter<String>(
+ getActivity(), android.R.layout.simple_spinner_item, encodingNames);
+ encodingSelection.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mEncodingSpinner.setAdapter(encodingSelection);
+ for (int i = 0; i < encodingNames.length; i++) {
+ if (mHost.getEncoding() != null &&
+ mHost.getEncoding().equals(data.get(encodingNames[i]))) {
+ mEncodingSpinner.setSelection(i);
+ break;
+ }
+ }
+ mEncodingSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ mHost.setEncoding(data.get(encodingNames[position]));
+ handleHostChange();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ }
}
private void setUriPartsContainerExpanded(boolean expanded) {
@@ -463,10 +550,39 @@ public class HostEditorFragment extends Fragment {
mHost.setHostname(host.getHostname());
mHost.setNickname(host.getNickname());
mHost.setPort(host.getPort());
+ handleHostChange();
+ }
+
+ /**
+ * Handles a change in the host caused by the user adjusting the values of one of the widgets
+ * in this fragment. If the change has resulted in a valid host, the new value is sent back
+ * to the listener; however, if the change ha resulted in an invalid host, the listener is
+ * notified.
+ */
+ private void handleHostChange() {
+ String protocol = (String) mTransportSpinner.getSelectedItem();
+ String quickConnectString = mQuickConnectField.getText().toString();
+ if (protocol == null || protocol.equals("") ||
+ quickConnectString == null || quickConnectString.equals("")) {
+ // Invalid protocol and/or string, so don't do anything.
+ mListener.onHostInvalidated();
+ return;
+ }
+
+ Uri uri = TransportFactory.getUri(protocol, quickConnectString);
+ if (uri == null) {
+ // Valid string, but does not accurately describe a URI.
+ mListener.onHostInvalidated();
+ return;
+ }
+
+ // Now, the host is confirmed to have a valid URI.
+ mListener.onValidHostConfigured(mHost);
}
public interface Listener {
- public void onHostUpdated(HostBean host);
+ public void onValidHostConfigured(HostBean host);
+ public void onHostInvalidated();
}
private class HostTextFieldWatcher implements TextWatcher {
@@ -515,6 +631,7 @@ public class HostEditorFragment extends Fragment {
mUriFieldEditInProgress = false;
}
}
+ handleHostChange();
}
private boolean isUriRelatedField(String fieldType) {
@@ -545,6 +662,7 @@ public class HostEditorFragment extends Fragment {
} else {
throw new RuntimeException("Invalid field type.");
}
+ handleHostChange();
}
}
}
diff --git a/app/src/main/java/org/connectbot/HostListActivity.java b/app/src/main/java/org/connectbot/HostListActivity.java
index 3ad8c55..509ef80 100644
--- a/app/src/main/java/org/connectbot/HostListActivity.java
+++ b/app/src/main/java/org/connectbot/HostListActivity.java
@@ -223,8 +223,8 @@ public class HostListActivity extends AppCompatListActivity implements OnHostSta
addHostButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- DialogFragment dialog = new AddHostDialogFragment();
- dialog.show(getSupportFragmentManager(), "AddHostDialogFragment");
+ Intent intent = EditHostActivity.createIntentForNewHost(HostListActivity.this);
+ startActivityForResult(intent, REQUEST_EDIT);
}
public void onNothingSelected(AdapterView<?> arg0) {}
@@ -439,8 +439,8 @@ public class HostListActivity extends AppCompatListActivity implements OnHostSta
MenuItem edit = menu.add(R.string.list_host_edit);
edit.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
- Intent intent = new Intent(HostListActivity.this, HostEditorActivity.class);
- intent.putExtra(Intent.EXTRA_TITLE, host.getId());
+ Intent intent = EditHostActivity.createIntentForExistingHost(
+ HostListActivity.this, host.getId());
HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
return true;
}
diff --git a/app/src/main/java/org/connectbot/TerminalView.java b/app/src/main/java/org/connectbot/TerminalView.java
index 7c4f51f..bc095fc 100644
--- a/app/src/main/java/org/connectbot/TerminalView.java
+++ b/app/src/main/java/org/connectbot/TerminalView.java
@@ -25,47 +25,72 @@ import org.connectbot.bean.SelectionArea;
import org.connectbot.service.FontSizeChangedListener;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalKeyListener;
+import org.connectbot.util.TerminalViewPager;
import android.annotation.TargetApi;
+import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelXorXfermode;
import android.graphics.RectF;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.support.v4.view.MotionEventCompat;
+import android.text.ClipboardManager;
+import android.view.ActionMode;
+import android.view.GestureDetector;
import android.view.InputDevice;
import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.MotionEvent;
-import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
import android.widget.Toast;
import de.mud.terminal.VDUBuffer;
import de.mud.terminal.vt320;
/**
- * User interface {@link View} for showing a TerminalBridge in an
+ * User interface {@link TextView} for showing a TerminalBridge in an
* {@link android.app.Activity}. Handles drawing bitmap updates and passing keystrokes down
* to terminal.
*
+ * On Honeycomb devices and above (>= APIv11), a TextView with transparent text (which is identical
+ * to the bitmap) is drawn above the bitmap. This TextView exists to allow the user to
+ * select and copy text.
+ *
* @author jsharkey
*/
-public class TerminalView extends View implements FontSizeChangedListener {
+public class TerminalView extends TextView implements FontSizeChangedListener {
private final Context context;
public final TerminalBridge bridge;
+
+ private final TerminalViewPager viewPager;
+ private GestureDetector gestureDetector;
+
+ private ClipboardManager clipboard;
+ private ActionMode selectionActionMode = null;
+ private String currentSelection = "";
+
+ // These are only used for pre-Honeycomb copying.
+ private int lastTouchedRow, lastTouchedCol;
+
private final Paint paint;
private final Paint cursorPaint;
private final Paint cursorStrokePaint;
@@ -96,17 +121,19 @@ public class TerminalView extends View implements FontSizeChangedListener {
private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
- public TerminalView(Context context, TerminalBridge bridge) {
+ public TerminalView(Context context, TerminalBridge bridge, TerminalViewPager pager) {
super(context);
this.context = context;
this.bridge = bridge;
- paint = new Paint();
+ this.viewPager = pager;
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
setFocusable(true);
setFocusableInTouchMode(true);
+ paint = new Paint();
+
cursorPaint = new Paint();
cursorPaint.setColor(bridge.color[bridge.defaultFg]);
cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
@@ -142,6 +169,7 @@ public class TerminalView extends View implements FontSizeChangedListener {
scaleMatrix = new Matrix();
bridge.addFontSizeChangedListener(this);
+ bridge.parentChanged(this);
// connect our view up to the bridge
setOnKeyListener(bridge.getKeyHandler());
@@ -150,6 +178,400 @@ public class TerminalView extends View implements FontSizeChangedListener {
// Enable accessibility features if a screen reader is active.
new AccessibilityStateTester().execute((Void) null);
+
+ clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ setTextColor(Color.TRANSPARENT);
+ setTypeface(Typeface.MONOSPACE);
+ onFontSizeChanged(bridge.getFontSize());
+
+ // Allow selection of and copying text for Honeycomb and above devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ setTextIsSelectable(true);
+ setCustomSelectionActionModeCallback(new TextSelectionActionModeCallback());
+ }
+
+ gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
+ private TerminalBridge bridge = TerminalView.this.bridge;
+ private 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;
+ }
+
+ totalY += distanceY;
+ final int moved = (int) (totalY / bridge.charHeight);
+
+ if (moved != 0) {
+ int base = bridge.buffer.getWindowBase();
+ bridge.buffer.setWindowBase(base + moved);
+ totalY = 0;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ viewPager.performClick();
+ return super.onSingleTapConfirmed(e);
+ }
+ });
+ }
+
+ @TargetApi(11)
+ private void closeSelectionActionMode() {
+ if (selectionActionMode != null) {
+ selectionActionMode.finish();
+ selectionActionMode = null;
+ }
+ }
+
+ public void copyCurrentSelectionToClipboard() {
+ ClipboardManager clipboard =
+ (ClipboardManager) TerminalView.this.context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (currentSelection.length() != 0) {
+ clipboard.setText(currentSelection);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ closeSelectionActionMode();
+ }
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if (selStart <= selEnd) {
+ currentSelection = getText().toString().substring(selStart, selEnd);
+ }
+ super.onSelectionChanged(selStart, selEnd);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // Selection may be beginning. Sync the TextView with the buffer.
+ refreshTextFromBuffer();
+ }
+
+ // Mouse input is treated differently:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
+ MotionEventCompat.getSource(event) == InputDevice.SOURCE_MOUSE) {
+ if (onMouseEvent(event, bridge)) {
+ return true;
+ }
+ viewPager.setPagingEnabled(true);
+ } else if (gestureDetector != null) {
+ // The gesture detector should not be called if touch event was from mouse.
+ gestureDetector.onTouchEvent(event);
+ }
+
+ // Old version of copying, only for pre-Honeycomb.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // when copying, highlight the area
+ if (bridge.isSelectingForCopy()) {
+ SelectionArea area = bridge.getSelectionArea();
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // recording starting area
+ viewPager.setPagingEnabled(false);
+ if (area.isSelectingOrigin()) {
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchedRow = row;
+ lastTouchedCol = col;
+ bridge.redraw();
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ /* ignore when user hasn't moved since last time so
+ * we can fine-tune with directional pad
+ */
+ if (row == lastTouchedRow && col == lastTouchedCol)
+ return true;
+
+ // if the user moves, start the selection for other corner
+ area.finishSelectingOrigin();
+
+ // update selected area
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchedRow = row;
+ lastTouchedCol = col;
+ bridge.redraw();
+ return true;
+ case MotionEvent.ACTION_UP:
+ /* If they didn't move their finger, maybe they meant to
+ * select the rest of the text with the directional pad.
+ */
+ if (area.getLeft() == area.getRight() &&
+ area.getTop() == area.getBottom()) {
+ return true;
+ }
+
+ // copy selected area to clipboard
+ String copiedText = area.copyFrom(bridge.buffer);
+
+ clipboard.setText(copiedText);
+ Toast.makeText(
+ context,
+ context.getString(R.string.console_copy_done, copiedText.length()),
+ Toast.LENGTH_LONG).show();
+
+ // fall through to clear state
+
+ case MotionEvent.ACTION_CANCEL:
+ // make sure we clear any highlighted area
+ area.reset();
+ bridge.setSelectingForCopy(false);
+ bridge.redraw();
+ viewPager.setPagingEnabled(true);
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ super.onTouchEvent(event);
+
+ return true;
+ }
+
+ @TargetApi(11)
+ private class TextSelectionActionModeCallback implements ActionMode.Callback {
+ private static final int COPY = 0;
+ private static final int PASTE = 1;
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ TerminalView.this.selectionActionMode = mode;
+
+ menu.clear();
+
+ menu.add(0, COPY, 0, R.string.console_menu_copy)
+ .setIcon(R.drawable.ic_action_copy)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_WITH_TEXT | MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ menu.add(0, PASTE, 1, R.string.console_menu_paste)
+ .setIcon(R.drawable.ic_action_paste)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_WITH_TEXT | MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case COPY:
+ copyCurrentSelectionToClipboard();
+ return true;
+ case PASTE:
+ String clip = clipboard.getText().toString();
+ TerminalView.this.bridge.injectString(clip);
+ mode.finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+ }
+
+ /**
+ * @param event
+ * @param bridge
+ * @return True if the event is handled.
+ */
+ @TargetApi(14)
+ private boolean onMouseEvent(MotionEvent event, TerminalBridge bridge) {
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+ int meta = event.getMetaState();
+ boolean shiftOn = (meta & KeyEvent.META_SHIFT_ON) != 0;
+ vt320 vtBuffer = (vt320) bridge.buffer;
+ boolean mouseReport = vtBuffer.isMouseReportEnabled();
+
+ // MouseReport can be "defeated" using the shift key.
+ if (!mouseReport || shiftOn) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (event.getButtonState() == MotionEvent.BUTTON_TERTIARY) {
+ // Middle click pastes.
+ String clip = clipboard.getText().toString();
+ bridge.injectString(clip);
+ return true;
+ }
+
+ // Begin "selection mode"
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ closeSelectionActionMode();
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ // In the middle of selection.
+
+ if (selectionActionMode == null) {
+ selectionActionMode = startActionMode(new TextSelectionActionModeCallback());
+ }
+
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int tempStart = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tempStart;
+ }
+
+ currentSelection = getText().toString().substring(selectionStart, selectionEnd);
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ viewPager.setPagingEnabled(false);
+ vtBuffer.mousePressed(
+ col, row, mouseEventToJavaModifiers(event));
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ viewPager.setPagingEnabled(true);
+ vtBuffer.mouseReleased(col, row);
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ int buttonState = event.getButtonState();
+ int button = (buttonState & MotionEvent.BUTTON_PRIMARY) != 0 ? 0 :
+ (buttonState & MotionEvent.BUTTON_SECONDARY) != 0 ? 1 :
+ (buttonState & MotionEvent.BUTTON_TERTIARY) != 0 ? 2 : 3;
+ vtBuffer.mouseMoved(
+ button,
+ col,
+ row,
+ (meta & KeyEvent.META_CTRL_ON) != 0,
+ (meta & KeyEvent.META_SHIFT_ON) != 0,
+ (meta & KeyEvent.META_META_ON) != 0);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Takes an android mouse event and produces a Java InputEvent modifiers int which can be
+ * passed to vt320.
+ * @param mouseEvent The {@link MotionEvent} which should be a mouse click or release.
+ * @return A Java InputEvent modifier int. See
+ * http://docs.oracle.com/javase/7/docs/api/java/awt/event/InputEvent.html
+ */
+ @TargetApi(14)
+ private static int mouseEventToJavaModifiers(MotionEvent mouseEvent) {
+ if (MotionEventCompat.getSource(mouseEvent) != InputDevice.SOURCE_MOUSE) return 0;
+
+ int mods = 0;
+
+ // See http://docs.oracle.com/javase/7/docs/api/constant-values.html
+ int buttonState = mouseEvent.getButtonState();
+ if ((buttonState & MotionEvent.BUTTON_PRIMARY) != 0)
+ mods |= 16;
+ if ((buttonState & MotionEvent.BUTTON_SECONDARY) != 0)
+ mods |= 8;
+ if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0)
+ mods |= 4;
+
+ // Note: Meta and Ctrl are intentionally swapped here to keep logic in vt320 simple.
+ int meta = mouseEvent.getMetaState();
+ if ((meta & KeyEvent.META_META_ON) != 0)
+ mods |= 2;
+ if ((meta & KeyEvent.META_SHIFT_ON) != 0)
+ mods |= 1;
+ if ((meta & KeyEvent.META_CTRL_ON) != 0)
+ mods |= 4;
+
+ return mods;
+ }
+
+ @Override
+ @TargetApi(12)
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if ((MotionEventCompat.getSource(event) & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL:
+ // Process scroll wheel movement:
+ float yDistance = MotionEventCompat.getAxisValue(event, MotionEvent.AXIS_VSCROLL);
+ vt320 vtBuffer = (vt320) bridge.buffer;
+ boolean mouseReport = vtBuffer.isMouseReportEnabled();
+ if (mouseReport) {
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+
+ vtBuffer.mouseWheel(
+ yDistance > 0,
+ col,
+ row,
+ (event.getMetaState() & KeyEvent.META_CTRL_ON) != 0,
+ (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0,
+ (event.getMetaState() & KeyEvent.META_META_ON) != 0);
+ return true;
+ } else if (yDistance != 0) {
+ int base = bridge.buffer.getWindowBase();
+ bridge.buffer.setWindowBase(base - Math.round(yDistance));
+ return true;
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ // TODO: cleanup and possibly optimize
+ private void refreshTextFromBuffer() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // Do not run this function because the textView is not selectable pre-Honeycomb.
+ return;
+ }
+
+ VDUBuffer vb = bridge.getVDUBuffer();
+
+ String line = "";
+ String buffer = "";
+
+ int windowBase = vb.getWindowBase();
+ int rowBegin = vb.getTopMargin();
+ int rowEnd = vb.getBottomMargin();
+ int numCols = vb.getColumns() - 1;
+
+ for (int r = rowBegin; r <= rowEnd; r++) {
+ for (int c = 0; c < numCols; c++) {
+ line += vb.charArray[windowBase + r][c];
+ }
+ buffer += line.replaceAll("\\s+$", "") + "\n";
+ line = "";
+ }
+
+ setText(buffer);
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public void startPreHoneycombCopyMode() {
+ // mark as copying and reset any previous bounds
+ SelectionArea area = bridge.getSelectionArea();
+ area.reset();
+ area.setBounds(bridge.buffer.getColumns(), bridge.buffer.getRows());
+
+ bridge.setSelectingForCopy(true);
+
+ // Make sure we show the initial selection
+ bridge.redraw();
}
public void destroy() {
@@ -166,8 +588,21 @@ public class TerminalView extends View implements FontSizeChangedListener {
scaleCursors();
}
- public void onFontSizeChanged(float size) {
+ public void onFontSizeChanged(final float size) {
scaleCursors();
+
+ ((Activity) context).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ setTextSize(size);
+
+ // For the TextView to line up with the bitmap text, lineHeight must be equal to
+ // the bridge's charHeight. See TextView.getLineHeight(), which has been reversed to
+ // derive lineSpacingMultiplier.
+ float lineSpacingMultiplier = (float) bridge.charHeight / getPaint().getFontMetricsInt(null);
+ setLineSpacing(0.0f, lineSpacingMultiplier);
+ }
+ });
}
private void scaleCursors() {
@@ -246,7 +681,8 @@ public class TerminalView extends View implements FontSizeChangedListener {
}
// draw any highlighted area
- if (bridge.isSelectingForCopy()) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB &&
+ bridge.isSelectingForCopy()) {
SelectionArea area = bridge.getSelectionArea();
canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(
@@ -259,6 +695,8 @@ public class TerminalView extends View implements FontSizeChangedListener {
canvas.restore();
}
}
+
+ super.onDraw(canvas);
}
public void notifyUser(String message) {
@@ -324,37 +762,6 @@ public class TerminalView extends View implements FontSizeChangedListener {
};
}
- @Override
- @TargetApi(12)
- public boolean onGenericMotionEvent(MotionEvent event) {
- if ((MotionEventCompat.getSource(event) & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_SCROLL:
- // Process scroll wheel movement:
- float yDistance = MotionEventCompat.getAxisValue(event, MotionEvent.AXIS_VSCROLL);
- boolean mouseReport = ((vt320) bridge.buffer).isMouseReportEnabled();
- if (mouseReport) {
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
-
- ((vt320) bridge.buffer).mouseWheel(
- yDistance > 0,
- col,
- row,
- (event.getMetaState() & KeyEvent.META_CTRL_ON) != 0,
- (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0,
- (event.getMetaState() & KeyEvent.META_META_ON) != 0);
- return true;
- } else if (yDistance != 0) {
- int base = bridge.buffer.getWindowBase();
- bridge.buffer.setWindowBase(base - Math.round(yDistance));
- return true;
- }
- }
- }
- return super.onGenericMotionEvent(event);
- }
-
public void propagateConsoleText(char[] rawText, int length) {
if (mAccessibilityActive) {
synchronized (mAccessibilityLock) {
diff --git a/app/src/main/java/org/connectbot/bean/HostBean.java b/app/src/main/java/org/connectbot/bean/HostBean.java
index d6cf8f4..438713e 100644
--- a/app/src/main/java/org/connectbot/bean/HostBean.java
+++ b/app/src/main/java/org/connectbot/bean/HostBean.java
@@ -47,7 +47,7 @@ public class HostBean extends AbstractBean {
private boolean useKeys = true;
private String useAuthAgent = HostDatabase.AUTHAGENT_NO;
private String postLogin = null;
- private long pubkeyId = -1;
+ private long pubkeyId = HostDatabase.PUBKEYID_ANY;
private boolean wantSession = true;
private String delKey = HostDatabase.DELKEY_DEL;
private int fontSize = DEFAULT_FONT_SIZE;
@@ -226,8 +226,8 @@ public class HostBean extends AbstractBean {
values.put(HostDatabase.FIELD_HOST_FONTSIZE, fontSize);
values.put(HostDatabase.FIELD_HOST_COMPRESSION, Boolean.toString(compression));
values.put(HostDatabase.FIELD_HOST_ENCODING, encoding);
- values.put(HostDatabase.FIELD_HOST_STAYCONNECTED, stayConnected);
- values.put(HostDatabase.FIELD_HOST_QUICKDISCONNECT, quickDisconnect);
+ values.put(HostDatabase.FIELD_HOST_STAYCONNECTED, Boolean.toString(stayConnected));
+ values.put(HostDatabase.FIELD_HOST_QUICKDISCONNECT, Boolean.toString(quickDisconnect));
return values;
}
diff --git a/app/src/main/java/org/connectbot/service/TerminalBridge.java b/app/src/main/java/org/connectbot/service/TerminalBridge.java
index b9e29e8..a888cc3 100644
--- a/app/src/main/java/org/connectbot/service/TerminalBridge.java
+++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java
@@ -341,6 +341,33 @@ public class TerminalBridge implements VDUDisplay {
}
/**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public void setSelectingForCopy(boolean selectingForCopy) {
+ this.selectingForCopy = selectingForCopy;
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public boolean isSelectingForCopy() {
+ return selectingForCopy;
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public SelectionArea getSelectionArea() {
+ return selectionArea;
+ }
+
+ public void copyCurrentSelection() {
+ if (parent != null) {
+ parent.copyCurrentSelectionToClipboard();
+ }
+ }
+
+ /**
* Inject a specific string into this terminal. Used for post-login strings
* and pasting clipboard.
*/
@@ -482,18 +509,6 @@ public class TerminalBridge implements VDUDisplay {
}
}
- public void setSelectingForCopy(boolean selectingForCopy) {
- this.selectingForCopy = selectingForCopy;
- }
-
- public boolean isSelectingForCopy() {
- return selectingForCopy;
- }
-
- public SelectionArea getSelectionArea() {
- return selectionArea;
- }
-
public synchronized void tryKeyVibrate() {
manager.tryKeyVibrate();
}
@@ -538,6 +553,10 @@ public class TerminalBridge implements VDUDisplay {
forcedSize = false;
}
+ public float getFontSize() {
+ return fontSizeDp;
+ }
+
/**
* Add an {@link FontSizeChangedListener} to the list of listeners for this
* bridge.
diff --git a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
index 1b2ffe4..753fa86 100644
--- a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
+++ b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
@@ -299,6 +299,14 @@ public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceCha
return true;
}
+ // CTRL-SHIFT-C to copy.
+ if (keyCode == KeyEvent.KEYCODE_C
+ && (derivedMetaState & HC_META_CTRL_ON) != 0
+ && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) {
+ bridge.copyCurrentSelection();
+ return true;
+ }
+
// CTRL-SHIFT-V to paste.
if (keyCode == KeyEvent.KEYCODE_V
&& (derivedMetaState & HC_META_CTRL_ON) != 0
diff --git a/app/src/main/java/org/connectbot/service/TerminalManager.java b/app/src/main/java/org/connectbot/service/TerminalManager.java
index 88c0811..e716094 100644
--- a/app/src/main/java/org/connectbot/service/TerminalManager.java
+++ b/app/src/main/java/org/connectbot/service/TerminalManager.java
@@ -257,7 +257,7 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen
}
public String getEmulation() {
- return prefs.getString(PreferenceConstants.EMULATION, "screen");
+ return prefs.getString(PreferenceConstants.EMULATION, "xterm-256color");
}
public int getScrollback() {
diff --git a/app/src/main/java/org/connectbot/transport/SSH.java b/app/src/main/java/org/connectbot/transport/SSH.java
index 89e2d8d..79b1d99 100644
--- a/app/src/main/java/org/connectbot/transport/SSH.java
+++ b/app/src/main/java/org/connectbot/transport/SSH.java
@@ -158,7 +158,7 @@ public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveC
else
algorithmName = serverHostKeyAlgorithm;
- switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
+ switch (hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
case KnownHosts.HOSTKEY_IS_OK:
bridge.outputLine(manager.res.getString(R.string.terminal_sucess, algorithmName, fingerprint));
return true;
diff --git a/app/src/main/java/org/connectbot/util/HostDatabase.java b/app/src/main/java/org/connectbot/util/HostDatabase.java
index 074e081..632e333 100644
--- a/app/src/main/java/org/connectbot/util/HostDatabase.java
+++ b/app/src/main/java/org/connectbot/util/HostDatabase.java
@@ -200,7 +200,7 @@ public class HostDatabase extends RobustSQLiteOpenHelper implements HostStorage,
+ FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "', "
+ FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', "
+ FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "', "
- + FIELD_HOST_STAYCONNECTED + " TEXT, "
+ + FIELD_HOST_STAYCONNECTED + " TEXT DEFAULT '" + Boolean.toString(false) + "', "
+ FIELD_HOST_QUICKDISCONNECT + " TEXT DEFAULT '" + Boolean.toString(false) + "')");
db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
diff --git a/app/src/main/java/org/connectbot/util/TerminalViewPager.java b/app/src/main/java/org/connectbot/util/TerminalViewPager.java
new file mode 100644
index 0000000..bb06b69
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/TerminalViewPager.java
@@ -0,0 +1,61 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2015 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import android.content.Context;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * Custom ViewPager {@link ViewPager} which is used to swipe between TerminalViews
+ * {@link org.connectbot.TerminalView}. Also allows temporary disabling of paging
+ * functionality to prevent event intercepts.
+ *
+ * @author rhansby
+ */
+public class TerminalViewPager extends ViewPager {
+ private boolean enabled;
+
+ public TerminalViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.enabled = true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (this.enabled) {
+ return super.onTouchEvent(event);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (this.enabled) {
+ return super.onInterceptTouchEvent(event);
+ }
+
+ return false;
+ }
+
+ public void setPagingEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/app/src/main/res/layout-large/act_console.xml b/app/src/main/res/layout-large/act_console.xml
index 6e7ab14..297d0b1 100644
--- a/app/src/main/res/layout-large/act_console.xml
+++ b/app/src/main/res/layout-large/act_console.xml
@@ -53,7 +53,7 @@
android:text="@string/terminal_no_hosts_connected"
android:textAppearance="?android:attr/textAppearanceMedium"/>
- <android.support.v4.view.ViewPager
+ <org.connectbot.util.TerminalViewPager
android:id="@+id/console_flip"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
diff --git a/app/src/main/res/layout/act_console.xml b/app/src/main/res/layout/act_console.xml
index 34f1d42..fea3a00 100644
--- a/app/src/main/res/layout/act_console.xml
+++ b/app/src/main/res/layout/act_console.xml
@@ -33,7 +33,7 @@
android:text="@string/terminal_no_hosts_connected"
android:textAppearance="?android:attr/textAppearanceMedium"/>
- <android.support.v4.view.ViewPager
+ <org.connectbot.util.TerminalViewPager
android:id="@+id/console_flip"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
diff --git a/app/src/main/res/menu/edit_host_activity_add_menu.xml b/app/src/main/res/menu/edit_host_activity_add_menu.xml
new file mode 100644
index 0000000..e793cf4
--- /dev/null
+++ b/app/src/main/res/menu/edit_host_activity_add_menu.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ ConnectBot: simple, powerful, open-source SSH client for Android
+ ~ Copyright 2015 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.
+ -->
+
+<menu
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:connectbot="http://schemas.android.com/apk/res-auto"
+ >
+
+ <item android:id="@+id/save"
+ android:icon="@drawable/ic_add"
+ android:title="@string/hostpref_add_host"
+ connectbot:showAsAction="always|withText"
+ />
+
+</menu>
diff --git a/app/src/main/res/menu/edit_host_activity_edit_menu.xml b/app/src/main/res/menu/edit_host_activity_edit_menu.xml
new file mode 100644
index 0000000..564e211
--- /dev/null
+++ b/app/src/main/res/menu/edit_host_activity_edit_menu.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ ConnectBot: simple, powerful, open-source SSH client for Android
+ ~ Copyright 2015 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.
+ -->
+
+<menu
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:connectbot="http://schemas.android.com/apk/res-auto"
+ >
+
+ <item android:id="@+id/save"
+ android:icon="@drawable/ic_add"
+ android:title="@string/hostpref_edit_host"
+ connectbot:showAsAction="always|withText"
+ />
+
+</menu>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index baf2db4..7d44f8d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -643,5 +643,9 @@
<string name="expand">Expand</string>
<!-- Label for checkbox which, when check, makes SSL authorization require confirmation. -->
<string name="hostpref_authagent_with_confirmation">require confirmation</string>
+ <!-- Text for button which, when clicked, adds a new host. -->
+ <string name="hostpref_add_host">Add host</string>
+ <!-- Text for button which, when clicked, saves an existing host. -->
+ <string name="hostpref_edit_host">Save host</string>
</resources>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index d1801c5..88b9855 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -50,7 +50,7 @@
android:summary="@string/pref_emulation_summary"
android:entries="@array/list_emulation_modes"
android:entryValues="@array/list_emulation_modes"
- android:defaultValue="screen"
+ android:defaultValue="xterm-256color"
/>
<EditTextPreference
diff --git a/config/quality.gradle b/config/quality.gradle
index 2df9dcb..d521b7a 100644
--- a/config/quality.gradle
+++ b/config/quality.gradle
@@ -1,5 +1,10 @@
apply plugin: 'checkstyle'
+checkstyle {
+ // This can't go past 6.7 until we upgrade to Gradle 2.7
+ toolVersion = "6.7"
+}
+
check.dependsOn 'checkstyle'
task checkstyle(type: Checkstyle) {