diff options
Diffstat (limited to 'app/src')
19 files changed, 1017 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 |