aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/org/connectbot/service
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2014-10-01 23:04:51 +0100
committerKenny Root <kenny@the-b.org>2014-10-01 12:48:19 +0100
commit49b779dcaf03e3598d2709b321e20ea029b25163 (patch)
tree05af547b1f1433d7dd6f7373d0b25a455e053a03 /app/src/main/java/org/connectbot/service
parentd64786d9197090c74072b648e487e3d34817bb57 (diff)
downloadconnectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.gz
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.bz2
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.zip
Convert to gradle build system
Diffstat (limited to 'app/src/main/java/org/connectbot/service')
-rw-r--r--app/src/main/java/org/connectbot/service/BackupAgent.java73
-rw-r--r--app/src/main/java/org/connectbot/service/BackupWrapper.java71
-rw-r--r--app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java22
-rw-r--r--app/src/main/java/org/connectbot/service/ConnectionNotifier.java192
-rw-r--r--app/src/main/java/org/connectbot/service/ConnectivityReceiver.java154
-rw-r--r--app/src/main/java/org/connectbot/service/FontSizeChangedListener.java31
-rw-r--r--app/src/main/java/org/connectbot/service/KeyEventUtil.java98
-rw-r--r--app/src/main/java/org/connectbot/service/PromptHelper.java159
-rw-r--r--app/src/main/java/org/connectbot/service/Relay.java145
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalBridge.java1018
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalKeyListener.java558
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalManager.java714
12 files changed, 3235 insertions, 0 deletions
diff --git a/app/src/main/java/org/connectbot/service/BackupAgent.java b/app/src/main/java/org/connectbot/service/BackupAgent.java
new file mode 100644
index 0000000..1e3bd81
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/BackupAgent.java
@@ -0,0 +1,73 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.io.IOException;
+
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.PubkeyDatabase;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FileBackupHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+/**
+ * @author kroot
+ *
+ */
+public class BackupAgent extends BackupAgentHelper {
+ @Override
+ public void onCreate() {
+ Log.d("ConnectBot.BackupAgent", "onCreate called");
+
+ SharedPreferencesBackupHelper prefs = new SharedPreferencesBackupHelper(this, getPackageName() + "_preferences");
+ addHelper(PreferenceConstants.BACKUP_PREF_KEY, prefs);
+
+ FileBackupHelper hosts = new FileBackupHelper(this, "../databases/" + HostDatabase.DB_NAME);
+ addHelper(HostDatabase.DB_NAME, hosts);
+
+ FileBackupHelper pubkeys = new FileBackupHelper(this, "../databases/" + PubkeyDatabase.DB_NAME);
+ addHelper(PubkeyDatabase.DB_NAME, pubkeys);
+
+ }
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ synchronized (HostDatabase.dbLock) {
+ super.onBackup(oldState, data, newState);
+ }
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+ Log.d("ConnectBot.BackupAgent", "onRestore called");
+
+ synchronized (HostDatabase.dbLock) {
+ Log.d("ConnectBot.BackupAgent", "onRestore in-lock");
+
+ super.onRestore(data, appVersionCode, newState);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/BackupWrapper.java b/app/src/main/java/org/connectbot/service/BackupWrapper.java
new file mode 100644
index 0000000..bfc7535
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/BackupWrapper.java
@@ -0,0 +1,71 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import org.connectbot.util.PreferenceConstants;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+
+/**
+ * @author kroot
+ *
+ */
+public abstract class BackupWrapper {
+ public static BackupWrapper getInstance() {
+ if (PreferenceConstants.PRE_FROYO)
+ return PreFroyo.Holder.sInstance;
+ else
+ return FroyoAndBeyond.Holder.sInstance;
+ }
+
+ public abstract void onDataChanged(Context context);
+
+ private static class PreFroyo extends BackupWrapper {
+ private static class Holder {
+ private static final PreFroyo sInstance = new PreFroyo();
+ }
+
+ @Override
+ public void onDataChanged(Context context) {
+ // do nothing for now
+ }
+ }
+
+ private static class FroyoAndBeyond extends BackupWrapper {
+ private static class Holder {
+ private static final FroyoAndBeyond sInstance = new FroyoAndBeyond();
+ }
+
+ private static BackupManager mBackupManager;
+
+ @Override
+ public void onDataChanged(Context context) {
+ checkBackupManager(context);
+ if (mBackupManager != null) {
+ mBackupManager.dataChanged();
+ }
+ }
+
+ private void checkBackupManager(Context context) {
+ if (mBackupManager == null) {
+ mBackupManager = new BackupManager(context);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java b/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java
new file mode 100644
index 0000000..21c41d1
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java
@@ -0,0 +1,22 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+public interface BridgeDisconnectedListener {
+ public void onDisconnected(TerminalBridge bridge);
+}
diff --git a/app/src/main/java/org/connectbot/service/ConnectionNotifier.java b/app/src/main/java/org/connectbot/service/ConnectionNotifier.java
new file mode 100644
index 0000000..d276761
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/ConnectionNotifier.java
@@ -0,0 +1,192 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.connectbot.ConsoleActivity;
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PreferenceConstants;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Color;
+
+/**
+ * @author Kenny Root
+ *
+ * Based on the concept from jasta's blog post.
+ */
+public abstract class ConnectionNotifier {
+ private static final int ONLINE_NOTIFICATION = 1;
+ private static final int ACTIVITY_NOTIFICATION = 2;
+
+ public static ConnectionNotifier getInstance() {
+ if (PreferenceConstants.PRE_ECLAIR)
+ return PreEclair.Holder.sInstance;
+ else
+ return EclairAndBeyond.Holder.sInstance;
+ }
+
+ protected NotificationManager getNotificationManager(Context context) {
+ return (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ protected Notification newNotification(Context context) {
+ Notification notification = new Notification();
+ notification.icon = R.drawable.notification_icon;
+ notification.when = System.currentTimeMillis();
+
+ return notification;
+ }
+
+ protected Notification newActivityNotification(Context context, HostBean host) {
+ Notification notification = newNotification(context);
+
+ Resources res = context.getResources();
+
+ String contentText = res.getString(
+ R.string.notification_text, host.getNickname());
+
+ Intent notificationIntent = new Intent(context, ConsoleActivity.class);
+ notificationIntent.setAction("android.intent.action.VIEW");
+ notificationIntent.setData(host.getUri());
+
+ PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
+ notificationIntent, 0);
+
+ notification.setLatestEventInfo(context, res.getString(R.string.app_name), contentText, contentIntent);
+
+ notification.flags = Notification.FLAG_AUTO_CANCEL;
+
+ notification.flags |= Notification.DEFAULT_LIGHTS;
+ if (HostDatabase.COLOR_RED.equals(host.getColor()))
+ notification.ledARGB = Color.RED;
+ else if (HostDatabase.COLOR_GREEN.equals(host.getColor()))
+ notification.ledARGB = Color.GREEN;
+ else if (HostDatabase.COLOR_BLUE.equals(host.getColor()))
+ notification.ledARGB = Color.BLUE;
+ else
+ notification.ledARGB = Color.WHITE;
+ notification.ledOnMS = 300;
+ notification.ledOffMS = 1000;
+ notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+
+ return notification;
+ }
+
+ protected Notification newRunningNotification(Context context) {
+ Notification notification = newNotification(context);
+
+ notification.flags = Notification.FLAG_ONGOING_EVENT
+ | Notification.FLAG_NO_CLEAR;
+ notification.when = 0;
+
+ notification.contentIntent = PendingIntent.getActivity(context,
+ ONLINE_NOTIFICATION,
+ new Intent(context, ConsoleActivity.class), 0);
+
+ Resources res = context.getResources();
+
+ notification.setLatestEventInfo(context,
+ res.getString(R.string.app_name),
+ res.getString(R.string.app_is_running),
+ notification.contentIntent);
+
+ return notification;
+ }
+
+ public void showActivityNotification(Service context, HostBean host) {
+ getNotificationManager(context).notify(ACTIVITY_NOTIFICATION, newActivityNotification(context, host));
+ }
+
+ public void hideActivityNotification(Service context) {
+ getNotificationManager(context).cancel(ACTIVITY_NOTIFICATION);
+ }
+
+ public abstract void showRunningNotification(Service context);
+ public abstract void hideRunningNotification(Service context);
+
+ private static class PreEclair extends ConnectionNotifier {
+ private static final Class<?>[] setForegroundSignature = new Class[] {boolean.class};
+ private Method setForeground = null;
+
+ private static class Holder {
+ private static final PreEclair sInstance = new PreEclair();
+ }
+
+ public PreEclair() {
+ try {
+ setForeground = Service.class.getMethod("setForeground", setForegroundSignature);
+ } catch (Exception e) {
+ }
+ }
+
+ @Override
+ public void showRunningNotification(Service context) {
+ if (setForeground != null) {
+ Object[] setForegroundArgs = new Object[1];
+ setForegroundArgs[0] = Boolean.TRUE;
+ try {
+ setForeground.invoke(context, setForegroundArgs);
+ } catch (InvocationTargetException e) {
+ } catch (IllegalAccessException e) {
+ }
+ getNotificationManager(context).notify(ONLINE_NOTIFICATION, newRunningNotification(context));
+ }
+ }
+
+ @Override
+ public void hideRunningNotification(Service context) {
+ if (setForeground != null) {
+ Object[] setForegroundArgs = new Object[1];
+ setForegroundArgs[0] = Boolean.FALSE;
+ try {
+ setForeground.invoke(context, setForegroundArgs);
+ } catch (InvocationTargetException e) {
+ } catch (IllegalAccessException e) {
+ }
+ getNotificationManager(context).cancel(ONLINE_NOTIFICATION);
+ }
+ }
+ }
+
+ private static class EclairAndBeyond extends ConnectionNotifier {
+ private static class Holder {
+ private static final EclairAndBeyond sInstance = new EclairAndBeyond();
+ }
+
+ @Override
+ public void showRunningNotification(Service context) {
+ context.startForeground(ONLINE_NOTIFICATION, newRunningNotification(context));
+ }
+
+ @Override
+ public void hideRunningNotification(Service context) {
+ context.stopForeground(true);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java b/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java
new file mode 100644
index 0000000..3248a2a
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java
@@ -0,0 +1,154 @@
+/**
+ *
+ */
+package org.connectbot.service;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.util.Log;
+
+/**
+ * @author kroot
+ *
+ */
+public class ConnectivityReceiver extends BroadcastReceiver {
+ private static final String TAG = "ConnectBot.ConnectivityManager";
+
+ private boolean mIsConnected = false;
+
+ final private TerminalManager mTerminalManager;
+
+ final private WifiLock mWifiLock;
+
+ private int mNetworkRef = 0;
+
+ private boolean mLockingWifi;
+
+ private Object[] mLock = new Object[0];
+
+ public ConnectivityReceiver(TerminalManager manager, boolean lockingWifi) {
+ mTerminalManager = manager;
+
+ final ConnectivityManager cm =
+ (ConnectivityManager) manager.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ final WifiManager wm = (WifiManager) manager.getSystemService(Context.WIFI_SERVICE);
+ mWifiLock = wm.createWifiLock(TAG);
+
+ final NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ mIsConnected = (info.getState() == State.CONNECTED);
+ }
+
+ mLockingWifi = lockingWifi;
+
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ manager.registerReceiver(this, filter);
+ }
+
+ /* (non-Javadoc)
+ * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ Log.w(TAG, "onReceived() called: " + intent);
+ return;
+ }
+
+ boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+ boolean isFailover = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);
+
+ Log.d(TAG, "onReceived() called; noConnectivity? " + noConnectivity + "; isFailover? " + isFailover);
+
+ if (noConnectivity && !isFailover && mIsConnected) {
+ mIsConnected = false;
+ mTerminalManager.onConnectivityLost();
+ } else if (!mIsConnected) {
+ NetworkInfo info = (NetworkInfo) intent.getExtras()
+ .get(ConnectivityManager.EXTRA_NETWORK_INFO);
+
+ if (mIsConnected = (info.getState() == State.CONNECTED)) {
+ mTerminalManager.onConnectivityRestored();
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ public void cleanup() {
+ if (mWifiLock.isHeld())
+ mWifiLock.release();
+
+ mTerminalManager.unregisterReceiver(this);
+ }
+
+ /**
+ * Increase the number of things using the network. Acquire a Wi-Fi lock
+ * if necessary.
+ */
+ public void incRef() {
+ synchronized (mLock) {
+ mNetworkRef += 1;
+
+ acquireWifiLockIfNecessaryLocked();
+ }
+ }
+
+ /**
+ * Decrease the number of things using the network. Release the Wi-Fi lock
+ * if necessary.
+ */
+ public void decRef() {
+ synchronized (mLock) {
+ mNetworkRef -= 1;
+
+ releaseWifiLockIfNecessaryLocked();
+ }
+ }
+
+ /**
+ * @param mLockingWifi
+ */
+ public void setWantWifiLock(boolean lockingWifi) {
+ synchronized (mLock) {
+ mLockingWifi = lockingWifi;
+
+ if (mLockingWifi) {
+ acquireWifiLockIfNecessaryLocked();
+ } else {
+ releaseWifiLockIfNecessaryLocked();
+ }
+ }
+ }
+
+ private void acquireWifiLockIfNecessaryLocked() {
+ if (mLockingWifi && mNetworkRef > 0 && !mWifiLock.isHeld()) {
+ mWifiLock.acquire();
+ }
+ }
+
+ private void releaseWifiLockIfNecessaryLocked() {
+ if (mNetworkRef == 0 && mWifiLock.isHeld()) {
+ mWifiLock.release();
+ }
+ }
+
+ /**
+ * @return whether we're connected to a network
+ */
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java b/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java
new file mode 100644
index 0000000..eb1c33d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java
@@ -0,0 +1,31 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public interface FontSizeChangedListener {
+
+ /**
+ * @param size
+ * new font size
+ */
+ void onFontSizeChanged(float size);
+}
diff --git a/app/src/main/java/org/connectbot/service/KeyEventUtil.java b/app/src/main/java/org/connectbot/service/KeyEventUtil.java
new file mode 100644
index 0000000..8ad645d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/KeyEventUtil.java
@@ -0,0 +1,98 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2014 Torne Wuff
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.connectbot.service;
+
+import android.view.KeyEvent;
+
+public class KeyEventUtil {
+ static final char CONTROL_LIMIT = ' ';
+ static final char PRINTABLE_LIMIT = '\u007e';
+ static final char[] HEX_DIGITS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ static String printableRepresentation(String source) {
+ if (source == null)
+ return null;
+
+ final StringBuilder sb = new StringBuilder();
+ final int limit = source.length();
+ char[] hexbuf = null;
+ int pointer = 0;
+
+ sb.append('"');
+ while (pointer < limit) {
+ int ch = source.charAt(pointer++);
+ switch (ch) {
+ case '\0':
+ sb.append("\\0");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ default:
+ if (CONTROL_LIMIT <= ch && ch <= PRINTABLE_LIMIT) {
+ sb.append((char) ch);
+ } else {
+ sb.append("\\u");
+ if (hexbuf == null)
+ hexbuf = new char[4];
+ for (int offs = 4; offs > 0; ) {
+ hexbuf[--offs] = HEX_DIGITS[ch & 0xf];
+ ch >>>= 4;
+ }
+ sb.append(hexbuf, 0, 4);
+ }
+ }
+ }
+ return sb.append('"').toString();
+ }
+
+ public static String describeKeyEvent(int keyCode, KeyEvent event) {
+ StringBuilder d = new StringBuilder();
+ d.append("keyCode=").append(keyCode);
+ d.append(", keyCodeToString=").append(KeyEvent.keyCodeToString(keyCode));
+ d.append(", event.toString=").append(event.toString());
+ d.append(", action=").append(event.getAction());
+ d.append(", characters=").append(printableRepresentation(event.getCharacters()));
+ d.append(", deviceId=").append(event.getDeviceId());
+ d.append(", displayLabel=").append((int) event.getDisplayLabel());
+ d.append(", flags=0x").append(Integer.toHexString(event.getFlags()));
+ d.append(", printingKey=").append(event.isPrintingKey());
+ d.append(", keyCode=").append(event.getKeyCode());
+ d.append(", metaState=0x").append(Integer.toHexString(event.getMetaState()));
+ d.append(", modifiers=0x").append(Integer.toHexString(event.getModifiers()));
+ d.append(", number=").append((int) event.getNumber());
+ d.append(", scanCode=").append(event.getScanCode());
+ d.append(", source=").append(event.getSource());
+ d.append(", unicodeChar=").append(event.getUnicodeChar());
+ return d.toString();
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/PromptHelper.java b/app/src/main/java/org/connectbot/service/PromptHelper.java
new file mode 100644
index 0000000..f0a37be
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/PromptHelper.java
@@ -0,0 +1,159 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.util.concurrent.Semaphore;
+
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * Helps provide a relay for prompts and responses between a possible user
+ * interface and some underlying service.
+ *
+ * @author jsharkey
+ */
+public class PromptHelper {
+ private final Object tag;
+
+ private Handler handler = null;
+
+ private Semaphore promptToken;
+ private Semaphore promptResponse;
+
+ public String promptInstructions = null;
+ public String promptHint = null;
+ public Object promptRequested = null;
+
+ private Object response = null;
+
+ public PromptHelper(Object tag) {
+ this.tag = tag;
+
+ // Threads must acquire this before they can send a prompt.
+ promptToken = new Semaphore(1);
+
+ // Responses will release this semaphore.
+ promptResponse = new Semaphore(0);
+ }
+
+
+ /**
+ * Register a user interface handler, if available.
+ */
+ public void setHandler(Handler handler) {
+ this.handler = handler;
+ }
+
+ /**
+ * Set an incoming value from an above user interface. Will automatically
+ * notify any waiting requests.
+ */
+ public void setResponse(Object value) {
+ response = value;
+ promptRequested = null;
+ promptInstructions = null;
+ promptHint = null;
+ promptResponse.release();
+ }
+
+ /**
+ * Return the internal response value just before erasing and returning it.
+ */
+ protected Object popResponse() {
+ Object value = response;
+ response = null;
+ return value;
+ }
+
+
+ /**
+ * Request a prompt response from parent. This is a blocking call until user
+ * interface returns a value.
+ * Only one thread can call this at a time. cancelPrompt() will force this to
+ * immediately return.
+ */
+ private Object requestPrompt(String instructions, String hint, Object type) throws InterruptedException {
+ Object response = null;
+
+ promptToken.acquire();
+
+ try {
+ promptInstructions = instructions;
+ promptHint = hint;
+ promptRequested = type;
+
+ // notify any parent watching for live events
+ if (handler != null)
+ Message.obtain(handler, -1, tag).sendToTarget();
+
+ // acquire lock until user passes back value
+ promptResponse.acquire();
+
+ response = popResponse();
+ } finally {
+ promptToken.release();
+ }
+
+ return response;
+ }
+
+ /**
+ * Request a string response from parent. This is a blocking call until user
+ * interface returns a value.
+ * @param hint prompt hint for user to answer
+ * @return string user has entered
+ */
+ public String requestStringPrompt(String instructions, String hint) {
+ String value = null;
+ try {
+ value = (String)this.requestPrompt(instructions, hint, String.class);
+ } catch(Exception e) {
+ }
+ return value;
+ }
+
+ /**
+ * Request a boolean response from parent. This is a blocking call until user
+ * interface returns a value.
+ * @param hint prompt hint for user to answer
+ * @return choice user has made (yes/no)
+ */
+ public Boolean requestBooleanPrompt(String instructions, String hint) {
+ Boolean value = null;
+ try {
+ value = (Boolean)this.requestPrompt(instructions, hint, Boolean.class);
+ } catch(Exception e) {
+ }
+ return value;
+ }
+
+ /**
+ * Cancel an in-progress prompt.
+ */
+ public void cancelPrompt() {
+ if (!promptToken.tryAcquire()) {
+ // A thread has the token, so try to interrupt it
+ response = null;
+ promptResponse.release();
+ } else {
+ // No threads have acquired the token
+ promptToken.release();
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/Relay.java b/app/src/main/java/org/connectbot/service/Relay.java
new file mode 100644
index 0000000..36672ec
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/Relay.java
@@ -0,0 +1,145 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+import org.apache.harmony.niochar.charset.additional.IBM437;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.util.EastAsianWidth;
+
+import android.util.Log;
+import de.mud.terminal.vt320;
+
+/**
+ * @author Kenny Root
+ */
+public class Relay implements Runnable {
+ private static final String TAG = "ConnectBot.Relay";
+
+ private static final int BUFFER_SIZE = 4096;
+
+ private TerminalBridge bridge;
+
+ private Charset currentCharset;
+ private CharsetDecoder decoder;
+
+ private AbsTransport transport;
+
+ private vt320 buffer;
+
+ private ByteBuffer byteBuffer;
+ private CharBuffer charBuffer;
+
+ private byte[] byteArray;
+ private char[] charArray;
+
+ public Relay(TerminalBridge bridge, AbsTransport transport, vt320 buffer, String encoding) {
+ setCharset(encoding);
+ this.bridge = bridge;
+ this.transport = transport;
+ this.buffer = buffer;
+ }
+
+ public void setCharset(String encoding) {
+ Log.d("ConnectBot.Relay", "changing charset to " + encoding);
+ Charset charset;
+ if (encoding.equals("CP437"))
+ charset = new IBM437("IBM437",
+ new String[] { "IBM437", "CP437" });
+ else
+ charset = Charset.forName(encoding);
+
+ if (charset == currentCharset || charset == null)
+ return;
+
+ CharsetDecoder newCd = charset.newDecoder();
+ newCd.onUnmappableCharacter(CodingErrorAction.REPLACE);
+ newCd.onMalformedInput(CodingErrorAction.REPLACE);
+
+ currentCharset = charset;
+ synchronized (this) {
+ decoder = newCd;
+ }
+ }
+
+ public Charset getCharset() {
+ return currentCharset;
+ }
+
+ public void run() {
+ byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
+ charBuffer = CharBuffer.allocate(BUFFER_SIZE);
+
+ /* for East Asian character widths */
+ byte[] wideAttribute = new byte[BUFFER_SIZE];
+
+ byteArray = byteBuffer.array();
+ charArray = charBuffer.array();
+
+ CoderResult result;
+
+ int bytesRead = 0;
+ byteBuffer.limit(0);
+ int bytesToRead;
+ int offset;
+ int charWidth;
+
+ EastAsianWidth measurer = EastAsianWidth.getInstance();
+
+ try {
+ while (true) {
+ charWidth = bridge.charWidth;
+ bytesToRead = byteBuffer.capacity() - byteBuffer.limit();
+ offset = byteBuffer.arrayOffset() + byteBuffer.limit();
+ bytesRead = transport.read(byteArray, offset, bytesToRead);
+
+ if (bytesRead > 0) {
+ byteBuffer.limit(byteBuffer.limit() + bytesRead);
+
+ synchronized (this) {
+ result = decoder.decode(byteBuffer, charBuffer, false);
+ }
+
+ if (result.isUnderflow() &&
+ byteBuffer.limit() == byteBuffer.capacity()) {
+ byteBuffer.compact();
+ byteBuffer.limit(byteBuffer.position());
+ byteBuffer.position(0);
+ }
+
+ offset = charBuffer.position();
+
+ measurer.measure(charArray, 0, offset, wideAttribute, bridge.defaultPaint, charWidth);
+ buffer.putString(charArray, wideAttribute, 0, charBuffer.position());
+ bridge.propagateConsoleText(charArray, charBuffer.position());
+ charBuffer.clear();
+ bridge.redraw();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Problem while handling incoming data in relay thread", e);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/TerminalBridge.java b/app/src/main/java/org/connectbot/service/TerminalBridge.java
new file mode 100644
index 0000000..6b87b74
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java
@@ -0,0 +1,1018 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.TerminalView;
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.bean.SelectionArea;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.transport.TransportFactory;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetrics;
+import android.graphics.Typeface;
+import android.text.ClipboardManager;
+import android.util.Log;
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.VDUDisplay;
+import de.mud.terminal.vt320;
+
+
+/**
+ * Provides a bridge between a MUD terminal buffer and a possible TerminalView.
+ * This separation allows us to keep the TerminalBridge running in a background
+ * service. A TerminalView shares down a bitmap that we can use for rendering
+ * when available.
+ *
+ * This class also provides SSH hostkey verification prompting, and password
+ * prompting.
+ */
+@SuppressWarnings("deprecation") // for ClipboardManager
+public class TerminalBridge implements VDUDisplay {
+ public final static String TAG = "ConnectBot.TerminalBridge";
+
+ public final static int DEFAULT_FONT_SIZE = 10;
+ private final static int FONT_SIZE_STEP = 2;
+
+ public Integer[] color;
+
+ public int defaultFg = HostDatabase.DEFAULT_FG_COLOR;
+ public int defaultBg = HostDatabase.DEFAULT_BG_COLOR;
+
+ protected final TerminalManager manager;
+
+ public HostBean host;
+
+ /* package */ AbsTransport transport;
+
+ final Paint defaultPaint;
+
+ private Relay relay;
+
+ private final String emulation;
+ private final int scrollback;
+
+ public Bitmap bitmap = null;
+ public VDUBuffer buffer = null;
+
+ private TerminalView parent = null;
+ private final Canvas canvas = new Canvas();
+
+ private boolean disconnected = false;
+ private boolean awaitingClose = false;
+
+ private boolean forcedSize = false;
+ private int columns;
+ private int rows;
+
+ /* package */ final TerminalKeyListener keyListener;
+
+ private boolean selectingForCopy = false;
+ private final SelectionArea selectionArea;
+
+ // TODO add support for the new clipboard API
+ private ClipboardManager clipboard;
+
+ public int charWidth = -1;
+ public int charHeight = -1;
+ private int charTop = -1;
+
+ private float fontSize = -1;
+
+ private final List<FontSizeChangedListener> fontSizeChangedListeners;
+
+ private final List<String> localOutput;
+
+ /**
+ * Flag indicating if we should perform a full-screen redraw during our next
+ * rendering pass.
+ */
+ private boolean fullRedraw = false;
+
+ public PromptHelper promptHelper;
+
+ protected BridgeDisconnectedListener disconnectListener = null;
+
+ /**
+ * Create a new terminal bridge suitable for unit testing.
+ */
+ public TerminalBridge() {
+ buffer = new vt320() {
+ @Override
+ public void write(byte[] b) {}
+ @Override
+ public void write(int b) {}
+ @Override
+ public void sendTelnetCommand(byte cmd) {}
+ @Override
+ public void setWindowSize(int c, int r) {}
+ @Override
+ public void debug(String s) {}
+ };
+
+ emulation = null;
+ manager = null;
+
+ defaultPaint = new Paint();
+
+ selectionArea = new SelectionArea();
+ scrollback = 1;
+
+ localOutput = new LinkedList<String>();
+
+ fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+ transport = null;
+
+ keyListener = new TerminalKeyListener(manager, this, buffer, null);
+ }
+
+ /**
+ * Create new terminal bridge with following parameters. We will immediately
+ * launch thread to start SSH connection and handle any hostkey verification
+ * and password authentication.
+ */
+ public TerminalBridge(final TerminalManager manager, final HostBean host) throws IOException {
+ this.manager = manager;
+ this.host = host;
+
+ emulation = manager.getEmulation();
+ scrollback = manager.getScrollback();
+
+ // create prompt helper to relay password and hostkey requests up to gui
+ promptHelper = new PromptHelper(this);
+
+ // create our default paint
+ defaultPaint = new Paint();
+ defaultPaint.setAntiAlias(true);
+ defaultPaint.setTypeface(Typeface.MONOSPACE);
+ defaultPaint.setFakeBoldText(true); // more readable?
+
+ localOutput = new LinkedList<String>();
+
+ fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+ int hostFontSize = host.getFontSize();
+ if (hostFontSize <= 0)
+ hostFontSize = DEFAULT_FONT_SIZE;
+ setFontSize(hostFontSize);
+
+ // create terminal buffer and handle outgoing data
+ // this is probably status reply information
+ buffer = new vt320() {
+ @Override
+ public void debug(String s) {
+ Log.d(TAG, s);
+ }
+
+ @Override
+ public void write(byte[] b) {
+ try {
+ if (b != null && transport != null)
+ transport.write(b);
+ } catch (IOException e) {
+ Log.e(TAG, "Problem writing outgoing data in vt320() thread", e);
+ }
+ }
+
+ @Override
+ public void write(int b) {
+ try {
+ if (transport != null)
+ transport.write(b);
+ } catch (IOException e) {
+ Log.e(TAG, "Problem writing outgoing data in vt320() thread", e);
+ }
+ }
+
+ // We don't use telnet sequences.
+ @Override
+ public void sendTelnetCommand(byte cmd) {
+ }
+
+ // We don't want remote to resize our window.
+ @Override
+ public void setWindowSize(int c, int r) {
+ }
+
+ @Override
+ public void beep() {
+ if (parent.isShown())
+ manager.playBeep();
+ else
+ manager.sendActivityNotification(host);
+ }
+ };
+
+ // Don't keep any scrollback if a session is not being opened.
+ if (host.getWantSession())
+ buffer.setBufferSize(scrollback);
+ else
+ buffer.setBufferSize(0);
+
+ resetColors();
+ buffer.setDisplay(this);
+
+ selectionArea = new SelectionArea();
+
+ keyListener = new TerminalKeyListener(manager, this, buffer, host.getEncoding());
+ }
+
+ public PromptHelper getPromptHelper() {
+ return promptHelper;
+ }
+
+ /**
+ * Spawn thread to open connection and start login process.
+ */
+ protected void startConnection() {
+ transport = TransportFactory.getTransport(host.getProtocol());
+ transport.setBridge(this);
+ transport.setManager(manager);
+ transport.setHost(host);
+
+ // TODO make this more abstract so we don't litter on AbsTransport
+ transport.setCompression(host.getCompression());
+ transport.setUseAuthAgent(host.getUseAuthAgent());
+ transport.setEmulation(emulation);
+
+ if (transport.canForwardPorts()) {
+ for (PortForwardBean portForward : manager.hostdb.getPortForwardsForHost(host))
+ transport.addPortForward(portForward);
+ }
+
+ outputLine(manager.res.getString(R.string.terminal_connecting, host.getHostname(), host.getPort(), host.getProtocol()));
+
+ Thread connectionThread = new Thread(new Runnable() {
+ public void run() {
+ transport.connect();
+ }
+ });
+ connectionThread.setName("Connection");
+ connectionThread.setDaemon(true);
+ connectionThread.start();
+ }
+
+ /**
+ * Handle challenges from keyboard-interactive authentication mode.
+ */
+ public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
+ String[] responses = new String[numPrompts];
+ for(int i = 0; i < numPrompts; i++) {
+ // request response from user for each prompt
+ responses[i] = promptHelper.requestStringPrompt(instruction, prompt[i]);
+ }
+ return responses;
+ }
+
+ /**
+ * @return charset in use by bridge
+ */
+ public Charset getCharset() {
+ return relay.getCharset();
+ }
+
+ /**
+ * Sets the encoding used by the terminal. If the connection is live,
+ * then the character set is changed for the next read.
+ * @param encoding the canonical name of the character encoding
+ */
+ public void setCharset(String encoding) {
+ if (relay != null)
+ relay.setCharset(encoding);
+ keyListener.setCharset(encoding);
+ }
+
+ /**
+ * Convenience method for writing a line into the underlying MUD buffer.
+ * Should never be called once the session is established.
+ */
+ public final void outputLine(String line) {
+ if (transport != null && transport.isSessionOpen())
+ Log.e(TAG, "Session established, cannot use outputLine!", new IOException("outputLine call traceback"));
+
+ synchronized (localOutput) {
+ final String s = line + "\r\n";
+
+ localOutput.add(s);
+
+ ((vt320) buffer).putString(s);
+
+ // For accessibility
+ final char[] charArray = s.toCharArray();
+ propagateConsoleText(charArray, charArray.length);
+ }
+ }
+
+ /**
+ * Inject a specific string into this terminal. Used for post-login strings
+ * and pasting clipboard.
+ */
+ public void injectString(final String string) {
+ if (string == null || string.length() == 0)
+ return;
+
+ Thread injectStringThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ transport.write(string.getBytes(host.getEncoding()));
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't inject string to remote host: ", e);
+ }
+ }
+ });
+ injectStringThread.setName("InjectString");
+ injectStringThread.start();
+ }
+
+ /**
+ * Internal method to request actual PTY terminal once we've finished
+ * authentication. If called before authenticated, it will just fail.
+ */
+ public void onConnected() {
+ disconnected = false;
+
+ ((vt320) buffer).reset();
+
+ // We no longer need our local output.
+ localOutput.clear();
+
+ // previously tried vt100 and xterm for emulation modes
+ // "screen" works the best for color and escape codes
+ ((vt320) buffer).setAnswerBack(emulation);
+
+ if (HostDatabase.DELKEY_BACKSPACE.equals(host.getDelKey()))
+ ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE);
+ else
+ ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL);
+
+ // create thread to relay incoming connection data to buffer
+ relay = new Relay(this, transport, (vt320) buffer, host.getEncoding());
+ Thread relayThread = new Thread(relay);
+ relayThread.setDaemon(true);
+ relayThread.setName("Relay");
+ relayThread.start();
+
+ // force font-size to make sure we resizePTY as needed
+ setFontSize(fontSize);
+
+ // finally send any post-login string, if requested
+ injectString(host.getPostLogin());
+ }
+
+ /**
+ * @return whether a session is open or not
+ */
+ public boolean isSessionOpen() {
+ if (transport != null)
+ return transport.isSessionOpen();
+ return false;
+ }
+
+ public void setOnDisconnectedListener(BridgeDisconnectedListener disconnectListener) {
+ this.disconnectListener = disconnectListener;
+ }
+
+ /**
+ * Force disconnection of this terminal bridge.
+ */
+ public void dispatchDisconnect(boolean immediate) {
+ // We don't need to do this multiple times.
+ synchronized (this) {
+ if (disconnected && !immediate)
+ return;
+
+ disconnected = true;
+ }
+
+ // Cancel any pending prompts.
+ promptHelper.cancelPrompt();
+
+ // disconnection request hangs if we havent really connected to a host yet
+ // temporary fix is to just spawn disconnection into a thread
+ Thread disconnectThread = new Thread(new Runnable() {
+ public void run() {
+ if (transport != null && transport.isConnected())
+ transport.close();
+ }
+ });
+ disconnectThread.setName("Disconnect");
+ disconnectThread.start();
+
+ if (immediate) {
+ awaitingClose = true;
+ if (disconnectListener != null)
+ disconnectListener.onDisconnected(TerminalBridge.this);
+ } else {
+ {
+ final String line = manager.res.getString(R.string.alert_disconnect_msg);
+ ((vt320) buffer).putString("\r\n" + line + "\r\n");
+ }
+ if (host.getStayConnected()) {
+ manager.requestReconnect(this);
+ return;
+ }
+ Thread disconnectPromptThread = new Thread(new Runnable() {
+ public void run() {
+ Boolean result = promptHelper.requestBooleanPrompt(null,
+ manager.res.getString(R.string.prompt_host_disconnected));
+ if (result == null || result.booleanValue()) {
+ awaitingClose = true;
+
+ // Tell the TerminalManager that we can be destroyed now.
+ if (disconnectListener != null)
+ disconnectListener.onDisconnected(TerminalBridge.this);
+ }
+ }
+ });
+ disconnectPromptThread.setName("DisconnectPrompt");
+ disconnectPromptThread.setDaemon(true);
+ disconnectPromptThread.start();
+ }
+ }
+
+ public void setSelectingForCopy(boolean selectingForCopy) {
+ this.selectingForCopy = selectingForCopy;
+ }
+
+ public boolean isSelectingForCopy() {
+ return selectingForCopy;
+ }
+
+ public SelectionArea getSelectionArea() {
+ return selectionArea;
+ }
+
+ public synchronized void tryKeyVibrate() {
+ manager.tryKeyVibrate();
+ }
+
+ /**
+ * Request a different font size. Will make call to parentChanged() to make
+ * sure we resize PTY if needed.
+ */
+ /* package */ final void setFontSize(float size) {
+ if (size <= 0.0)
+ return;
+
+ defaultPaint.setTextSize(size);
+ fontSize = size;
+
+ // read new metrics to get exact pixel dimensions
+ FontMetrics fm = defaultPaint.getFontMetrics();
+ charTop = (int)Math.ceil(fm.top);
+
+ float[] widths = new float[1];
+ defaultPaint.getTextWidths("X", widths);
+ charWidth = (int)Math.ceil(widths[0]);
+ charHeight = (int)Math.ceil(fm.descent - fm.top);
+
+ // refresh any bitmap with new font size
+ if(parent != null)
+ parentChanged(parent);
+
+ for (FontSizeChangedListener ofscl : fontSizeChangedListeners)
+ ofscl.onFontSizeChanged(size);
+
+ host.setFontSize((int) fontSize);
+ manager.hostdb.updateFontSize(host);
+
+ forcedSize = false;
+ }
+
+ /**
+ * Add an {@link FontSizeChangedListener} to the list of listeners for this
+ * bridge.
+ *
+ * @param listener
+ * listener to add
+ */
+ public void addFontSizeChangedListener(FontSizeChangedListener listener) {
+ fontSizeChangedListeners.add(listener);
+ }
+
+ /**
+ * Remove an {@link FontSizeChangedListener} from the list of listeners for
+ * this bridge.
+ *
+ * @param listener
+ */
+ public void removeFontSizeChangedListener(FontSizeChangedListener listener) {
+ fontSizeChangedListeners.remove(listener);
+ }
+
+ /**
+ * Something changed in our parent {@link TerminalView}, maybe it's a new
+ * parent, or maybe it's an updated font size. We should recalculate
+ * terminal size information and request a PTY resize.
+ */
+ public final synchronized void parentChanged(TerminalView parent) {
+ if (manager != null && !manager.isResizeAllowed()) {
+ Log.d(TAG, "Resize is not allowed now");
+ return;
+ }
+
+ this.parent = parent;
+ final int width = parent.getWidth();
+ final int height = parent.getHeight();
+
+ // Something has gone wrong with our layout; we're 0 width or height!
+ if (width <= 0 || height <= 0)
+ return;
+
+ clipboard = (ClipboardManager) parent.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ keyListener.setClipboardManager(clipboard);
+
+ if (!forcedSize) {
+ // recalculate buffer size
+ int newColumns, newRows;
+
+ newColumns = width / charWidth;
+ newRows = height / charHeight;
+
+ // If nothing has changed in the terminal dimensions and not an intial
+ // draw then don't blow away scroll regions and such.
+ if (newColumns == columns && newRows == rows)
+ return;
+
+ columns = newColumns;
+ rows = newRows;
+ }
+
+ // reallocate new bitmap if needed
+ boolean newBitmap = (bitmap == null);
+ if(bitmap != null)
+ newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height);
+
+ if (newBitmap) {
+ discardBitmap();
+ bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+ canvas.setBitmap(bitmap);
+ }
+
+ // clear out any old buffer information
+ defaultPaint.setColor(Color.BLACK);
+ canvas.drawPaint(defaultPaint);
+
+ // Stroke the border of the terminal if the size is being forced;
+ if (forcedSize) {
+ int borderX = (columns * charWidth) + 1;
+ int borderY = (rows * charHeight) + 1;
+
+ defaultPaint.setColor(Color.GRAY);
+ defaultPaint.setStrokeWidth(0.0f);
+ if (width >= borderX)
+ canvas.drawLine(borderX, 0, borderX, borderY + 1, defaultPaint);
+ if (height >= borderY)
+ canvas.drawLine(0, borderY, borderX + 1, borderY, defaultPaint);
+ }
+
+ try {
+ // request a terminal pty resize
+ synchronized (buffer) {
+ buffer.setScreenSize(columns, rows, true);
+ }
+
+ if(transport != null)
+ transport.setDimensions(columns, rows, width, height);
+ } catch(Exception e) {
+ Log.e(TAG, "Problem while trying to resize screen or PTY", e);
+ }
+
+ // redraw local output if we don't have a sesson to receive our resize request
+ if (transport == null) {
+ synchronized (localOutput) {
+ ((vt320) buffer).reset();
+
+ for (String line : localOutput)
+ ((vt320) buffer).putString(line);
+ }
+ }
+
+ // force full redraw with new buffer size
+ fullRedraw = true;
+ redraw();
+
+ parent.notifyUser(String.format("%d x %d", columns, rows));
+
+ Log.i(TAG, String.format("parentChanged() now width=%d, height=%d", columns, rows));
+ }
+
+ /**
+ * Somehow our parent {@link TerminalView} was destroyed. Now we don't need
+ * to redraw anywhere, and we can recycle our internal bitmap.
+ */
+ public synchronized void parentDestroyed() {
+ parent = null;
+ discardBitmap();
+ }
+
+ private void discardBitmap() {
+ if (bitmap != null)
+ bitmap.recycle();
+ bitmap = null;
+ }
+
+ public void setVDUBuffer(VDUBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ public VDUBuffer getVDUBuffer() {
+ return buffer;
+ }
+
+ public void propagateConsoleText(char[] rawText, int length) {
+ if (parent != null) {
+ parent.propagateConsoleText(rawText, length);
+ }
+ }
+
+ public void onDraw() {
+ int fg, bg;
+ synchronized (buffer) {
+ boolean entireDirty = buffer.update[0] || fullRedraw;
+ boolean isWideCharacter = false;
+
+ // walk through all lines in the buffer
+ for(int l = 0; l < buffer.height; l++) {
+
+ // check if this line is dirty and needs to be repainted
+ // also check for entire-buffer dirty flags
+ if (!entireDirty && !buffer.update[l + 1]) continue;
+
+ // reset dirty flag for this line
+ buffer.update[l + 1] = false;
+
+ // walk through all characters in this line
+ for (int c = 0; c < buffer.width; c++) {
+ int addr = 0;
+ int currAttr = buffer.charAttributes[buffer.windowBase + l][c];
+
+ {
+ int fgcolor = defaultFg;
+
+ // check if foreground color attribute is set
+ if ((currAttr & VDUBuffer.COLOR_FG) != 0)
+ fgcolor = ((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1;
+
+ if (fgcolor < 8 && (currAttr & VDUBuffer.BOLD) != 0)
+ fg = color[fgcolor + 8];
+ else
+ fg = color[fgcolor];
+ }
+
+ // check if background color attribute is set
+ if ((currAttr & VDUBuffer.COLOR_BG) != 0)
+ bg = color[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1];
+ else
+ bg = color[defaultBg];
+
+ // support character inversion by swapping background and foreground color
+ if ((currAttr & VDUBuffer.INVERT) != 0) {
+ int swapc = bg;
+ bg = fg;
+ fg = swapc;
+ }
+
+ // set underlined attributes if requested
+ defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0);
+
+ isWideCharacter = (currAttr & VDUBuffer.FULLWIDTH) != 0;
+
+ if (isWideCharacter)
+ addr++;
+ else {
+ // determine the amount of continuous characters with the same settings and print them all at once
+ while(c + addr < buffer.width
+ && buffer.charAttributes[buffer.windowBase + l][c + addr] == currAttr) {
+ addr++;
+ }
+ }
+
+ // Save the current clip region
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+
+ // clear this dirty area with background color
+ defaultPaint.setColor(bg);
+ if (isWideCharacter) {
+ canvas.clipRect(c * charWidth,
+ l * charHeight,
+ (c + 2) * charWidth,
+ (l + 1) * charHeight);
+ } else {
+ canvas.clipRect(c * charWidth,
+ l * charHeight,
+ (c + addr) * charWidth,
+ (l + 1) * charHeight);
+ }
+ canvas.drawPaint(defaultPaint);
+
+ // write the text string starting at 'c' for 'addr' number of characters
+ defaultPaint.setColor(fg);
+ if((currAttr & VDUBuffer.INVISIBLE) == 0)
+ canvas.drawText(buffer.charArray[buffer.windowBase + l], c,
+ addr, c * charWidth, (l * charHeight) - charTop,
+ defaultPaint);
+
+ // Restore the previous clip region
+ canvas.restore();
+
+ // advance to the next text block with different characteristics
+ c += addr - 1;
+ if (isWideCharacter)
+ c++;
+ }
+ }
+
+ // reset entire-buffer flags
+ buffer.update[0] = false;
+ }
+ fullRedraw = false;
+ }
+
+ public void redraw() {
+ if (parent != null)
+ parent.postInvalidate();
+ }
+
+ // We don't have a scroll bar.
+ public void updateScrollBar() {
+ }
+
+ /**
+ * Resize terminal to fit [rows]x[cols] in screen of size [width]x[height]
+ * @param rows
+ * @param cols
+ * @param width
+ * @param height
+ */
+ public synchronized void resizeComputed(int cols, int rows, int width, int height) {
+ float size = 8.0f;
+ float step = 8.0f;
+ float limit = 0.125f;
+
+ int direction;
+
+ while ((direction = fontSizeCompare(size, cols, rows, width, height)) < 0)
+ size += step;
+
+ if (direction == 0) {
+ Log.d("fontsize", String.format("Found match at %f", size));
+ return;
+ }
+
+ step /= 2.0f;
+ size -= step;
+
+ while ((direction = fontSizeCompare(size, cols, rows, width, height)) != 0
+ && step >= limit) {
+ step /= 2.0f;
+ if (direction > 0) {
+ size -= step;
+ } else {
+ size += step;
+ }
+ }
+
+ if (direction > 0)
+ size -= step;
+
+ this.columns = cols;
+ this.rows = rows;
+ setFontSize(size);
+ forcedSize = true;
+ }
+
+ private int fontSizeCompare(float size, int cols, int rows, int width, int height) {
+ // read new metrics to get exact pixel dimensions
+ defaultPaint.setTextSize(size);
+ FontMetrics fm = defaultPaint.getFontMetrics();
+
+ float[] widths = new float[1];
+ defaultPaint.getTextWidths("X", widths);
+ int termWidth = (int)widths[0] * cols;
+ int termHeight = (int)Math.ceil(fm.descent - fm.top) * rows;
+
+ Log.d("fontsize", String.format("font size %f resulted in %d x %d", size, termWidth, termHeight));
+
+ // Check to see if it fits in resolution specified.
+ if (termWidth > width || termHeight > height)
+ return 1;
+
+ if (termWidth == width || termHeight == height)
+ return 0;
+
+ return -1;
+ }
+
+ /**
+ * @return whether underlying transport can forward ports
+ */
+ public boolean canFowardPorts() {
+ return transport.canForwardPorts();
+ }
+
+ /**
+ * Adds the {@link PortForwardBean} to the list.
+ * @param portForward the port forward bean to add
+ * @return true on successful addition
+ */
+ public boolean addPortForward(PortForwardBean portForward) {
+ return transport.addPortForward(portForward);
+ }
+
+ /**
+ * Removes the {@link PortForwardBean} from the list.
+ * @param portForward the port forward bean to remove
+ * @return true on successful removal
+ */
+ public boolean removePortForward(PortForwardBean portForward) {
+ return transport.removePortForward(portForward);
+ }
+
+ /**
+ * @return the list of port forwards
+ */
+ public List<PortForwardBean> getPortForwards() {
+ return transport.getPortForwards();
+ }
+
+ /**
+ * Enables a port forward member. After calling this method, the port forward should
+ * be operational.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward setup
+ */
+ public boolean enablePortForward(PortForwardBean portForward) {
+ if (!transport.isConnected()) {
+ Log.i(TAG, "Attempt to enable port forward while not connected");
+ return false;
+ }
+
+ return transport.enablePortForward(portForward);
+ }
+
+ /**
+ * Disables a port forward member. After calling this method, the port forward should
+ * be non-functioning.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward tear-down
+ */
+ public boolean disablePortForward(PortForwardBean portForward) {
+ if (!transport.isConnected()) {
+ Log.i(TAG, "Attempt to disable port forward while not connected");
+ return false;
+ }
+
+ return transport.disablePortForward(portForward);
+ }
+
+ /**
+ * @return whether the TerminalBridge should close
+ */
+ public boolean isAwaitingClose() {
+ return awaitingClose;
+ }
+
+ /**
+ * @return whether this connection had started and subsequently disconnected
+ */
+ public boolean isDisconnected() {
+ return disconnected;
+ }
+
+ /* (non-Javadoc)
+ * @see de.mud.terminal.VDUDisplay#setColor(byte, byte, byte, byte)
+ */
+ public void setColor(int index, int red, int green, int blue) {
+ // Don't allow the system colors to be overwritten for now. May violate specs.
+ if (index < color.length && index >= 16)
+ color[index] = 0xff000000 | red << 16 | green << 8 | blue;
+ }
+
+ public final void resetColors() {
+ int[] defaults = manager.hostdb.getDefaultColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME);
+ defaultFg = defaults[0];
+ defaultBg = defaults[1];
+
+ color = manager.hostdb.getColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME);
+ }
+
+ private static Pattern urlPattern = null;
+
+ /**
+ * @return
+ */
+ public List<String> scanForURLs() {
+ List<String> urls = new LinkedList<String>();
+
+ if (urlPattern == null) {
+ // based on http://www.ietf.org/rfc/rfc2396.txt
+ String scheme = "[A-Za-z][-+.0-9A-Za-z]*";
+ String unreserved = "[-._~0-9A-Za-z]";
+ String pctEncoded = "%[0-9A-Fa-f]{2}";
+ String subDelims = "[!$&'()*+,;:=]";
+ String userinfo = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|:)*";
+ String h16 = "[0-9A-Fa-f]{1,4}";
+ String decOctet = "(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])";
+ String ipv4address = decOctet + "\\." + decOctet + "\\." + decOctet + "\\." + decOctet;
+ String ls32 = "(?:" + h16 + ":" + h16 + "|" + ipv4address + ")";
+ String ipv6address = "(?:(?:" + h16 + "){6}" + ls32 + ")";
+ String ipvfuture = "v[0-9A-Fa-f]+.(?:" + unreserved + "|" + subDelims + "|:)+";
+ String ipLiteral = "\\[(?:" + ipv6address + "|" + ipvfuture + ")\\]";
+ String regName = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + ")*";
+ String host = "(?:" + ipLiteral + "|" + ipv4address + "|" + regName + ")";
+ String port = "[0-9]*";
+ String authority = "(?:" + userinfo + "@)?" + host + "(?::" + port + ")?";
+ String pchar = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|@)";
+ String segment = pchar + "*";
+ String pathAbempty = "(?:/" + segment + ")*";
+ String segmentNz = pchar + "+";
+ String pathAbsolute = "/(?:" + segmentNz + "(?:/" + segment + ")*)?";
+ String pathRootless = segmentNz + "(?:/" + segment + ")*";
+ String hierPart = "(?://" + authority + pathAbempty + "|" + pathAbsolute + "|" + pathRootless + ")";
+ String query = "(?:" + pchar + "|/|\\?)*";
+ String fragment = "(?:" + pchar + "|/|\\?)*";
+ String uriRegex = scheme + ":" + hierPart + "(?:" + query + ")?(?:#" + fragment + ")?";
+ urlPattern = Pattern.compile(uriRegex);
+ }
+
+ char[] visibleBuffer = new char[buffer.height * buffer.width];
+ for (int l = 0; l < buffer.height; l++)
+ System.arraycopy(buffer.charArray[buffer.windowBase + l], 0,
+ visibleBuffer, l * buffer.width, buffer.width);
+
+ Matcher urlMatcher = urlPattern.matcher(new String(visibleBuffer));
+ while (urlMatcher.find())
+ urls.add(urlMatcher.group());
+
+ return urls;
+ }
+
+ /**
+ * @return
+ */
+ public boolean isUsingNetwork() {
+ return transport.usesNetwork();
+ }
+
+ /**
+ * @return
+ */
+ public TerminalKeyListener getKeyHandler() {
+ return keyListener;
+ }
+
+ /**
+ *
+ */
+ public void resetScrollPosition() {
+ // if we're in scrollback, scroll to bottom of window on input
+ if (buffer.windowBase != buffer.screenBase)
+ buffer.setWindowBase(buffer.screenBase);
+ }
+
+ /**
+ *
+ */
+ public void increaseFontSize() {
+ setFontSize(fontSize + FONT_SIZE_STEP);
+ }
+
+ /**
+ *
+ */
+ public void decreaseFontSize() {
+ setFontSize(fontSize - FONT_SIZE_STEP);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
new file mode 100644
index 0000000..7ff21df
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
@@ -0,0 +1,558 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.connectbot.service;
+
+import java.io.IOException;
+
+import org.connectbot.TerminalView;
+import org.connectbot.bean.SelectionArea;
+import org.connectbot.util.PreferenceConstants;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Configuration;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.vt320;
+
+/**
+ * @author kenny
+ *
+ */
+@SuppressWarnings("deprecation") // for ClipboardManager
+public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener {
+ private static final String TAG = "ConnectBot.OnKeyListener";
+
+ // Constants for our private tracking of modifier state
+ public final static int OUR_CTRL_ON = 0x01;
+ public final static int OUR_CTRL_LOCK = 0x02;
+ public final static int OUR_ALT_ON = 0x04;
+ public final static int OUR_ALT_LOCK = 0x08;
+ public final static int OUR_SHIFT_ON = 0x10;
+ public final static int OUR_SHIFT_LOCK = 0x20;
+ private final static int OUR_SLASH = 0x40;
+ private final static int OUR_TAB = 0x80;
+
+ // All the transient key codes
+ private final static int OUR_TRANSIENT = OUR_CTRL_ON | OUR_ALT_ON
+ | OUR_SHIFT_ON | OUR_SLASH | OUR_TAB;
+
+ // The bit mask of momentary and lock states for each
+ private final static int OUR_CTRL_MASK = OUR_CTRL_ON | OUR_CTRL_LOCK;
+ private final static int OUR_ALT_MASK = OUR_ALT_ON | OUR_ALT_LOCK;
+ private final static int OUR_SHIFT_MASK = OUR_SHIFT_ON | OUR_SHIFT_LOCK;
+
+ // backport constants from api level 11
+ private final static int KEYCODE_ESCAPE = 111;
+ private final static int HC_META_CTRL_ON = 0x1000;
+ private final static int HC_META_CTRL_LEFT_ON = 0x2000;
+ private final static int HC_META_CTRL_RIGHT_ON = 0x4000;
+ private final static int HC_META_CTRL_MASK = HC_META_CTRL_ON | HC_META_CTRL_RIGHT_ON
+ | HC_META_CTRL_LEFT_ON;
+ private final static int HC_META_ALT_MASK = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+ | KeyEvent.META_ALT_RIGHT_ON;
+
+ private final TerminalManager manager;
+ private final TerminalBridge bridge;
+ private final VDUBuffer buffer;
+
+ private String keymode = null;
+ private final boolean deviceHasHardKeyboard;
+ private boolean shiftedNumbersAreFKeysOnHardKeyboard;
+ private boolean controlNumbersAreFKeysOnSoftKeyboard;
+ private boolean volumeKeysChangeFontSize;
+
+ private int ourMetaState = 0;
+
+ private int mDeadKey = 0;
+
+ // TODO add support for the new API.
+ private ClipboardManager clipboard = null;
+
+ private boolean selectingForCopy = false;
+ private final SelectionArea selectionArea;
+
+ private String encoding;
+
+ private final SharedPreferences prefs;
+
+ public TerminalKeyListener(TerminalManager manager,
+ TerminalBridge bridge,
+ VDUBuffer buffer,
+ String encoding) {
+ this.manager = manager;
+ this.bridge = bridge;
+ this.buffer = buffer;
+ this.encoding = encoding;
+
+ selectionArea = new SelectionArea();
+
+ prefs = PreferenceManager.getDefaultSharedPreferences(manager);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ deviceHasHardKeyboard = (manager.res.getConfiguration().keyboard
+ == Configuration.KEYBOARD_QWERTY);
+
+ updatePrefs();
+ }
+
+ /**
+ * Handle onKey() events coming down from a {@link TerminalView} above us.
+ * Modify the keys to make more sense to a host then pass it to the transport.
+ */
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ try {
+ // skip keys if we aren't connected yet or have been disconnected
+ if (bridge.isDisconnected() || bridge.transport == null)
+ return false;
+
+ final boolean interpretAsHardKeyboard = deviceHasHardKeyboard &&
+ !manager.hardKeyboardHidden;
+ final boolean rightModifiersAreSlashAndTab = interpretAsHardKeyboard &&
+ PreferenceConstants.KEYMODE_RIGHT.equals(keymode);
+ final boolean leftModifiersAreSlashAndTab = interpretAsHardKeyboard &&
+ PreferenceConstants.KEYMODE_LEFT.equals(keymode);
+ final boolean shiftedNumbersAreFKeys = shiftedNumbersAreFKeysOnHardKeyboard &&
+ interpretAsHardKeyboard;
+ final boolean controlNumbersAreFKeys = controlNumbersAreFKeysOnSoftKeyboard &&
+ !interpretAsHardKeyboard;
+
+ // Ignore all key-up events except for the special keys
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ if (rightModifiersAreSlashAndTab) {
+ if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ && (ourMetaState & OUR_SLASH) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write('/');
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
+ && (ourMetaState & OUR_TAB) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write(0x09);
+ return true;
+ }
+ } else if (leftModifiersAreSlashAndTab) {
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
+ && (ourMetaState & OUR_SLASH) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write('/');
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
+ && (ourMetaState & OUR_TAB) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.transport.write(0x09);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ //Log.i("CBKeyDebug", KeyEventUtil.describeKeyEvent(keyCode, event));
+
+ if (volumeKeysChangeFontSize) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ bridge.increaseFontSize();
+ return true;
+ } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+ bridge.decreaseFontSize();
+ return true;
+ }
+ }
+
+ bridge.resetScrollPosition();
+
+ // Handle potentially multi-character IME input.
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
+ event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+ byte[] input = event.getCharacters().getBytes(encoding);
+ bridge.transport.write(input);
+ return true;
+ }
+
+ /// Handle alt and shift keys if they aren't repeating
+ if (event.getRepeatCount() == 0) {
+ if (rightModifiersAreSlashAndTab) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ ourMetaState |= OUR_SLASH;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ ourMetaState |= OUR_TAB;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ }
+ } else if (leftModifiersAreSlashAndTab) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ ourMetaState |= OUR_SLASH;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ ourMetaState |= OUR_TAB;
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ }
+ } else {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ metaPress(OUR_ALT_ON);
+ return true;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ metaPress(OUR_SHIFT_ON);
+ return true;
+ }
+ }
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ if (selectingForCopy) {
+ if (selectionArea.isSelectingOrigin())
+ selectionArea.finishSelectingOrigin();
+ else {
+ if (clipboard != null) {
+ // copy selected area to clipboard
+ String copiedText = selectionArea.copyFrom(buffer);
+ clipboard.setText(copiedText);
+ // XXX STOPSHIP
+// manager.notifyUser(manager.getString(
+// R.string.console_copy_done,
+// copiedText.length()));
+ selectingForCopy = false;
+ selectionArea.reset();
+ }
+ }
+ } else {
+ if ((ourMetaState & OUR_CTRL_ON) != 0) {
+ sendEscape();
+ ourMetaState &= ~OUR_CTRL_ON;
+ } else
+ metaPress(OUR_CTRL_ON);
+ }
+ bridge.redraw();
+ return true;
+ }
+
+ int derivedMetaState = event.getMetaState();
+ if ((ourMetaState & OUR_SHIFT_MASK) != 0)
+ derivedMetaState |= KeyEvent.META_SHIFT_ON;
+ if ((ourMetaState & OUR_ALT_MASK) != 0)
+ derivedMetaState |= KeyEvent.META_ALT_ON;
+ if ((ourMetaState & OUR_CTRL_MASK) != 0)
+ derivedMetaState |= HC_META_CTRL_ON;
+
+ if ((ourMetaState & OUR_TRANSIENT) != 0) {
+ ourMetaState &= ~OUR_TRANSIENT;
+ bridge.redraw();
+ }
+
+ // Test for modified numbers becoming function keys
+ if (shiftedNumbersAreFKeys && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) {
+ if (sendFunctionKey(keyCode))
+ return true;
+ }
+ if (controlNumbersAreFKeys && (derivedMetaState & HC_META_CTRL_ON) != 0) {
+ if (sendFunctionKey(keyCode))
+ return true;
+ }
+
+ // Ask the system to use the keymap to give us the unicode character for this key,
+ // with our derived modifier state applied.
+ int uchar = event.getUnicodeChar(derivedMetaState & ~HC_META_CTRL_MASK);
+ int ucharWithoutAlt = event.getUnicodeChar(
+ derivedMetaState & ~(HC_META_ALT_MASK | HC_META_CTRL_MASK));
+ if (uchar != ucharWithoutAlt) {
+ // The alt key was used to modify the character returned; therefore, drop the alt
+ // modifier from the state so we don't end up sending alt+key.
+ derivedMetaState &= ~HC_META_ALT_MASK;
+ }
+
+ // Remove shift from the modifier state as it has already been used by getUnicodeChar.
+ derivedMetaState &= ~KeyEvent.META_SHIFT_ON;
+
+ if ((uchar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+ mDeadKey = uchar & KeyCharacterMap.COMBINING_ACCENT_MASK;
+ return true;
+ }
+
+ if (mDeadKey != 0) {
+ uchar = KeyCharacterMap.getDeadChar(mDeadKey, keyCode);
+ mDeadKey = 0;
+ }
+
+ // If we have a defined non-control character
+ if (uchar >= 0x20) {
+ if ((derivedMetaState & HC_META_CTRL_ON) != 0)
+ uchar = keyAsControl(uchar);
+ if ((derivedMetaState & KeyEvent.META_ALT_ON) != 0)
+ sendEscape();
+ if (uchar < 0x80)
+ bridge.transport.write(uchar);
+ else
+ // TODO write encoding routine that doesn't allocate each time
+ bridge.transport.write(new String(Character.toChars(uchar))
+ .getBytes(encoding));
+ return true;
+ }
+
+ // look for special chars
+ switch(keyCode) {
+ case KEYCODE_ESCAPE:
+ sendEscape();
+ return true;
+ case KeyEvent.KEYCODE_TAB:
+ bridge.transport.write(0x09);
+ return true;
+ case KeyEvent.KEYCODE_CAMERA:
+
+ // check to see which shortcut the camera button triggers
+ String camera = manager.prefs.getString(
+ PreferenceConstants.CAMERA,
+ PreferenceConstants.CAMERA_CTRLA_SPACE);
+ if(PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
+ bridge.transport.write(0x01);
+ bridge.transport.write(' ');
+ } else if(PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
+ bridge.transport.write(0x01);
+ } else if(PreferenceConstants.CAMERA_ESC.equals(camera)) {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ } else if(PreferenceConstants.CAMERA_ESC_A.equals(camera)) {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ bridge.transport.write('a');
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_DEL:
+ ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ',
+ getStateForBuffer());
+ return true;
+ case KeyEvent.KEYCODE_ENTER:
+ ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', 0);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (selectingForCopy) {
+ selectionArea.decrementColumn();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (selectingForCopy) {
+ selectionArea.decrementRow();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (selectingForCopy) {
+ selectionArea.incrementRow();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (selectingForCopy) {
+ selectionArea.incrementColumn();
+ bridge.redraw();
+ } else {
+ ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ',
+ getStateForBuffer());
+ bridge.tryKeyVibrate();
+ }
+ return true;
+ }
+
+ } catch (IOException e) {
+ Log.e(TAG, "Problem while trying to handle an onKey() event", e);
+ try {
+ bridge.transport.flush();
+ } catch (IOException ioe) {
+ Log.d(TAG, "Our transport was closed, dispatching disconnect event");
+ bridge.dispatchDisconnect(false);
+ }
+ } catch (NullPointerException npe) {
+ Log.d(TAG, "Input before connection established ignored.");
+ return true;
+ }
+
+ return false;
+ }
+
+ public int keyAsControl(int key) {
+ // Support CTRL-a through CTRL-z
+ if (key >= 0x61 && key <= 0x7A)
+ key -= 0x60;
+ // Support CTRL-A through CTRL-_
+ else if (key >= 0x41 && key <= 0x5F)
+ key -= 0x40;
+ // CTRL-space sends NULL
+ else if (key == 0x20)
+ key = 0x00;
+ // CTRL-? sends DEL
+ else if (key == 0x3F)
+ key = 0x7F;
+ return key;
+ }
+
+ public void sendEscape() {
+ ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+ }
+
+ /**
+ * @param key
+ * @return successful
+ */
+ private boolean sendFunctionKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_1:
+ ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_2:
+ ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_3:
+ ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_4:
+ ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_5:
+ ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_6:
+ ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_7:
+ ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_8:
+ ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_9:
+ ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0);
+ return true;
+ case KeyEvent.KEYCODE_0:
+ ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Handle meta key presses where the key can be locked on.
+ * <p>
+ * 1st press: next key to have meta state<br />
+ * 2nd press: meta state is locked on<br />
+ * 3rd press: disable meta state
+ *
+ * @param code
+ */
+ public void metaPress(int code) {
+ if ((ourMetaState & (code << 1)) != 0) {
+ ourMetaState &= ~(code << 1);
+ } else if ((ourMetaState & code) != 0) {
+ ourMetaState &= ~code;
+ ourMetaState |= code << 1;
+ } else
+ ourMetaState |= code;
+ bridge.redraw();
+ }
+
+ public void setTerminalKeyMode(String keymode) {
+ this.keymode = keymode;
+ }
+
+ private int getStateForBuffer() {
+ int bufferState = 0;
+
+ if ((ourMetaState & OUR_CTRL_MASK) != 0)
+ bufferState |= vt320.KEY_CONTROL;
+ if ((ourMetaState & OUR_SHIFT_MASK) != 0)
+ bufferState |= vt320.KEY_SHIFT;
+ if ((ourMetaState & OUR_ALT_MASK) != 0)
+ bufferState |= vt320.KEY_ALT;
+
+ return bufferState;
+ }
+
+ public int getMetaState() {
+ return ourMetaState;
+ }
+
+ public int getDeadKey() {
+ return mDeadKey;
+ }
+
+ public void setClipboardManager(ClipboardManager clipboard) {
+ this.clipboard = clipboard;
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (PreferenceConstants.KEYMODE.equals(key) ||
+ PreferenceConstants.SHIFT_FKEYS.equals(key) ||
+ PreferenceConstants.CTRL_FKEYS.equals(key) ||
+ PreferenceConstants.VOLUME_FONT.equals(key)) {
+ updatePrefs();
+ }
+ }
+
+ private void updatePrefs() {
+ keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
+ shiftedNumbersAreFKeysOnHardKeyboard =
+ prefs.getBoolean(PreferenceConstants.SHIFT_FKEYS, false);
+ controlNumbersAreFKeysOnSoftKeyboard =
+ prefs.getBoolean(PreferenceConstants.CTRL_FKEYS, false);
+ volumeKeysChangeFontSize = prefs.getBoolean(PreferenceConstants.VOLUME_FONT, true);
+ }
+
+ public void setCharset(String encoding) {
+ this.encoding = encoding;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/service/TerminalManager.java b/app/src/main/java/org/connectbot/service/TerminalManager.java
new file mode 100644
index 0000000..369d79a
--- /dev/null
+++ b/app/src/main/java/org/connectbot/service/TerminalManager.java
@@ -0,0 +1,714 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.service;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.transport.TransportFactory;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * Manager for SSH connections that runs as a service. This service holds a list
+ * of currently connected SSH bridges that are ready for connection up to a GUI
+ * if needed.
+ *
+ * @author jsharkey
+ */
+public class TerminalManager extends Service implements BridgeDisconnectedListener, OnSharedPreferenceChangeListener {
+ public final static String TAG = "ConnectBot.TerminalManager";
+
+ public List<TerminalBridge> bridges = new LinkedList<TerminalBridge>();
+ public Map<HostBean, WeakReference<TerminalBridge>> mHostBridgeMap =
+ new HashMap<HostBean, WeakReference<TerminalBridge>>();
+ public Map<String, WeakReference<TerminalBridge>> mNicknameBridgeMap =
+ new HashMap<String, WeakReference<TerminalBridge>>();
+
+ public TerminalBridge defaultBridge = null;
+
+ public List<HostBean> disconnected = new LinkedList<HostBean>();
+
+ public Handler disconnectHandler = null;
+
+ public Map<String, KeyHolder> loadedKeypairs = new HashMap<String, KeyHolder>();
+
+ public Resources res;
+
+ public HostDatabase hostdb;
+ public PubkeyDatabase pubkeydb;
+
+ protected SharedPreferences prefs;
+
+ final private IBinder binder = new TerminalBinder();
+
+ private ConnectivityReceiver connectivityManager;
+
+ private MediaPlayer mediaPlayer;
+
+ private Timer pubkeyTimer;
+
+ private Timer idleTimer;
+ private final long IDLE_TIMEOUT = 300000; // 5 minutes
+
+ private Vibrator vibrator;
+ private volatile boolean wantKeyVibration;
+ public static final long VIBRATE_DURATION = 30;
+
+ private boolean wantBellVibration;
+
+ private boolean resizeAllowed = true;
+
+ private boolean savingKeys;
+
+ protected List<WeakReference<TerminalBridge>> mPendingReconnect
+ = new LinkedList<WeakReference<TerminalBridge>>();
+
+ public boolean hardKeyboardHidden;
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "Starting service");
+
+ prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ res = getResources();
+
+ pubkeyTimer = new Timer("pubkeyTimer", true);
+
+ hostdb = new HostDatabase(this);
+ pubkeydb = new PubkeyDatabase(this);
+
+ // load all marked pubkeys into memory
+ updateSavingKeys();
+ List<PubkeyBean> pubkeys = pubkeydb.getAllStartPubkeys();
+
+ for (PubkeyBean pubkey : pubkeys) {
+ try {
+ PrivateKey privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType());
+ PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
+ KeyPair pair = new KeyPair(pubKey, privKey);
+
+ addKey(pubkey, pair);
+ } catch (Exception e) {
+ Log.d(TAG, String.format("Problem adding key '%s' to in-memory cache", pubkey.getNickname()), e);
+ }
+ }
+
+ vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ wantKeyVibration = prefs.getBoolean(PreferenceConstants.BUMPY_ARROWS, true);
+
+ wantBellVibration = prefs.getBoolean(PreferenceConstants.BELL_VIBRATE, true);
+ enableMediaPlayer();
+
+ hardKeyboardHidden = (res.getConfiguration().hardKeyboardHidden ==
+ Configuration.HARDKEYBOARDHIDDEN_YES);
+
+ final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true);
+
+ connectivityManager = new ConnectivityReceiver(this, lockingWifi);
+
+ }
+
+ private void updateSavingKeys() {
+ savingKeys = prefs.getBoolean(PreferenceConstants.MEMKEYS, true);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "Destroying service");
+
+ disconnectAll(true);
+
+ if(hostdb != null) {
+ hostdb.close();
+ hostdb = null;
+ }
+
+ if(pubkeydb != null) {
+ pubkeydb.close();
+ pubkeydb = null;
+ }
+
+ synchronized (this) {
+ if (idleTimer != null)
+ idleTimer.cancel();
+ if (pubkeyTimer != null)
+ pubkeyTimer.cancel();
+ }
+
+ connectivityManager.cleanup();
+
+ ConnectionNotifier.getInstance().hideRunningNotification(this);
+
+ disableMediaPlayer();
+ }
+
+ /**
+ * Disconnect all currently connected bridges.
+ */
+ private void disconnectAll(final boolean immediate) {
+ TerminalBridge[] tmpBridges = null;
+
+ synchronized (bridges) {
+ if (bridges.size() > 0) {
+ tmpBridges = bridges.toArray(new TerminalBridge[bridges.size()]);
+ }
+ }
+
+ if (tmpBridges != null) {
+ // disconnect and dispose of any existing bridges
+ for (int i = 0; i < tmpBridges.length; i++)
+ tmpBridges[i].dispatchDisconnect(immediate);
+ }
+ }
+
+ /**
+ * Open a new SSH session using the given parameters.
+ */
+ private TerminalBridge openConnection(HostBean host) throws IllegalArgumentException, IOException {
+ // throw exception if terminal already open
+ if (getConnectedBridge(host) != null) {
+ throw new IllegalArgumentException("Connection already open for that nickname");
+ }
+
+ TerminalBridge bridge = new TerminalBridge(this, host);
+ bridge.setOnDisconnectedListener(this);
+ bridge.startConnection();
+
+ synchronized (bridges) {
+ bridges.add(bridge);
+ WeakReference<TerminalBridge> wr = new WeakReference<TerminalBridge>(bridge);
+ mHostBridgeMap.put(bridge.host, wr);
+ mNicknameBridgeMap.put(bridge.host.getNickname(), wr);
+ }
+
+ synchronized (disconnected) {
+ disconnected.remove(bridge.host);
+ }
+
+ if (bridge.isUsingNetwork()) {
+ connectivityManager.incRef();
+ }
+
+ if (prefs.getBoolean(PreferenceConstants.CONNECTION_PERSIST, true)) {
+ ConnectionNotifier.getInstance().showRunningNotification(this);
+ }
+
+ // also update database with new connected time
+ touchHost(host);
+
+ return bridge;
+ }
+
+ public String getEmulation() {
+ return prefs.getString(PreferenceConstants.EMULATION, "screen");
+ }
+
+ public int getScrollback() {
+ int scrollback = 140;
+ try {
+ scrollback = Integer.parseInt(prefs.getString(PreferenceConstants.SCROLLBACK, "140"));
+ } catch(Exception e) {
+ }
+ return scrollback;
+ }
+
+ /**
+ * Open a new connection by reading parameters from the given URI. Follows
+ * format specified by an individual transport.
+ */
+ public TerminalBridge openConnection(Uri uri) throws Exception {
+ HostBean host = TransportFactory.findHost(hostdb, uri);
+
+ if (host == null)
+ host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
+
+ return openConnection(host);
+ }
+
+ /**
+ * Update the last-connected value for the given nickname by passing through
+ * to {@link HostDatabase}.
+ */
+ private void touchHost(HostBean host) {
+ hostdb.touchHost(host);
+ }
+
+ /**
+ * Find a connected {@link TerminalBridge} with the given HostBean.
+ *
+ * @param host the HostBean to search for
+ * @return TerminalBridge that uses the HostBean
+ */
+ public TerminalBridge getConnectedBridge(HostBean host) {
+ WeakReference<TerminalBridge> wr = mHostBridgeMap.get(host);
+ if (wr != null) {
+ return wr.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Find a connected {@link TerminalBridge} using its nickname.
+ *
+ * @param nickname
+ * @return TerminalBridge that matches nickname
+ */
+ public TerminalBridge getConnectedBridge(final String nickname) {
+ if (nickname == null) {
+ return null;
+ }
+ WeakReference<TerminalBridge> wr = mNicknameBridgeMap.get(nickname);
+ if (wr != null) {
+ return wr.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Called by child bridge when somehow it's been disconnected.
+ */
+ public void onDisconnected(TerminalBridge bridge) {
+ boolean shouldHideRunningNotification = false;
+
+ synchronized (bridges) {
+ // remove this bridge from our list
+ bridges.remove(bridge);
+
+ mHostBridgeMap.remove(bridge.host);
+ mNicknameBridgeMap.remove(bridge.host.getNickname());
+
+ if (bridge.isUsingNetwork()) {
+ connectivityManager.decRef();
+ }
+
+ if (bridges.size() == 0 &&
+ mPendingReconnect.size() == 0) {
+ shouldHideRunningNotification = true;
+ }
+ }
+
+ synchronized (disconnected) {
+ disconnected.add(bridge.host);
+ }
+
+ if (shouldHideRunningNotification) {
+ ConnectionNotifier.getInstance().hideRunningNotification(this);
+ }
+
+ // pass notification back up to gui
+ if (disconnectHandler != null)
+ Message.obtain(disconnectHandler, -1, bridge).sendToTarget();
+ }
+
+ public boolean isKeyLoaded(String nickname) {
+ return loadedKeypairs.containsKey(nickname);
+ }
+
+ public void addKey(PubkeyBean pubkey, KeyPair pair) {
+ addKey(pubkey, pair, false);
+ }
+
+ public void addKey(PubkeyBean pubkey, KeyPair pair, boolean force) {
+ if (!savingKeys && !force)
+ return;
+
+ removeKey(pubkey.getNickname());
+
+ byte[] sshPubKey = PubkeyUtils.extractOpenSSHPublic(pair);
+
+ KeyHolder keyHolder = new KeyHolder();
+ keyHolder.bean = pubkey;
+ keyHolder.pair = pair;
+ keyHolder.openSSHPubkey = sshPubKey;
+
+ loadedKeypairs.put(pubkey.getNickname(), keyHolder);
+
+ if (pubkey.getLifetime() > 0) {
+ final String nickname = pubkey.getNickname();
+ pubkeyTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Log.d(TAG, "Unloading from memory key: " + nickname);
+ removeKey(nickname);
+ }
+ }, pubkey.getLifetime() * 1000);
+ }
+
+ Log.d(TAG, String.format("Added key '%s' to in-memory cache", pubkey.getNickname()));
+ }
+
+ public boolean removeKey(String nickname) {
+ Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname));
+ return loadedKeypairs.remove(nickname) != null;
+ }
+
+ public boolean removeKey(byte[] publicKey) {
+ String nickname = null;
+ for (Entry<String,KeyHolder> entry : loadedKeypairs.entrySet()) {
+ if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) {
+ nickname = entry.getKey();
+ break;
+ }
+ }
+
+ if (nickname != null) {
+ Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname));
+ return removeKey(nickname);
+ } else
+ return false;
+ }
+
+ public KeyPair getKey(String nickname) {
+ if (loadedKeypairs.containsKey(nickname)) {
+ KeyHolder keyHolder = loadedKeypairs.get(nickname);
+ return keyHolder.pair;
+ } else
+ return null;
+ }
+
+ public KeyPair getKey(byte[] publicKey) {
+ for (KeyHolder keyHolder : loadedKeypairs.values()) {
+ if (Arrays.equals(keyHolder.openSSHPubkey, publicKey))
+ return keyHolder.pair;
+ }
+ return null;
+ }
+
+ public String getKeyNickname(byte[] publicKey) {
+ for (Entry<String,KeyHolder> entry : loadedKeypairs.entrySet()) {
+ if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey))
+ return entry.getKey();
+ }
+ return null;
+ }
+
+ private void stopWithDelay() {
+ // TODO add in a way to check whether keys loaded are encrypted and only
+ // set timer when we have an encrypted key loaded
+
+ if (loadedKeypairs.size() > 0) {
+ synchronized (this) {
+ if (idleTimer == null)
+ idleTimer = new Timer("idleTimer", true);
+
+ idleTimer.schedule(new IdleTask(), IDLE_TIMEOUT);
+ }
+ } else {
+ Log.d(TAG, "Stopping service immediately");
+ stopSelf();
+ }
+ }
+
+ protected void stopNow() {
+ if (bridges.size() == 0) {
+ stopSelf();
+ }
+ }
+
+ private synchronized void stopIdleTimer() {
+ if (idleTimer != null) {
+ idleTimer.cancel();
+ idleTimer = null;
+ }
+ }
+
+ public class TerminalBinder extends Binder {
+ public TerminalManager getService() {
+ return TerminalManager.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "Someone bound to TerminalManager");
+
+ setResizeAllowed(true);
+
+ stopIdleTimer();
+
+ // Make sure we stay running to maintain the bridges
+ startService(new Intent(this, TerminalManager.class));
+
+ return binder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ /*
+ * We want this service to continue running until it is explicitly
+ * stopped, so return sticky.
+ */
+ return START_STICKY;
+ }
+
+ @Override
+ public void onRebind(Intent intent) {
+ super.onRebind(intent);
+
+ setResizeAllowed(true);
+
+ Log.i(TAG, "Someone rebound to TerminalManager");
+
+ stopIdleTimer();
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ Log.i(TAG, "Someone unbound from TerminalManager");
+
+ setResizeAllowed(true);
+
+ if (bridges.size() == 0) {
+ stopWithDelay();
+ }
+
+ return true;
+ }
+
+ private class IdleTask extends TimerTask {
+ /* (non-Javadoc)
+ * @see java.util.TimerTask#run()
+ */
+ @Override
+ public void run() {
+ Log.d(TAG, String.format("Stopping service after timeout of ~%d seconds", IDLE_TIMEOUT / 1000));
+ TerminalManager.this.stopNow();
+ }
+ }
+
+ public void tryKeyVibrate() {
+ if (wantKeyVibration)
+ vibrate();
+ }
+
+ private void vibrate() {
+ if (vibrator != null)
+ vibrator.vibrate(VIBRATE_DURATION);
+ }
+
+ private void enableMediaPlayer() {
+ mediaPlayer = new MediaPlayer();
+
+ float volume = prefs.getFloat(PreferenceConstants.BELL_VOLUME,
+ PreferenceConstants.DEFAULT_BELL_VOLUME);
+
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
+ mediaPlayer.setOnCompletionListener(new BeepListener());
+
+ AssetFileDescriptor file = res.openRawResourceFd(R.raw.bell);
+ try {
+ mediaPlayer.setDataSource(file.getFileDescriptor(), file
+ .getStartOffset(), file.getLength());
+ file.close();
+ mediaPlayer.setVolume(volume, volume);
+ mediaPlayer.prepare();
+ } catch (IOException e) {
+ Log.e(TAG, "Error setting up bell media player", e);
+ }
+ }
+
+ private void disableMediaPlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+ }
+
+ public void playBeep() {
+ if (mediaPlayer != null)
+ mediaPlayer.start();
+
+ if (wantBellVibration)
+ vibrate();
+ }
+
+ private static class BeepListener implements OnCompletionListener {
+ public void onCompletion(MediaPlayer mp) {
+ mp.seekTo(0);
+ }
+ }
+
+ /**
+ * Send system notification to user for a certain host. When user selects
+ * the notification, it will bring them directly to the ConsoleActivity
+ * displaying the host.
+ *
+ * @param host
+ */
+ public void sendActivityNotification(HostBean host) {
+ if (!prefs.getBoolean(PreferenceConstants.BELL_NOTIFICATION, false))
+ return;
+
+ ConnectionNotifier.getInstance().showActivityNotification(this, host);
+ }
+
+ /* (non-Javadoc)
+ * @see android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged(android.content.SharedPreferences, java.lang.String)
+ */
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (PreferenceConstants.BELL.equals(key)) {
+ boolean wantAudible = sharedPreferences.getBoolean(
+ PreferenceConstants.BELL, true);
+ if (wantAudible && mediaPlayer == null)
+ enableMediaPlayer();
+ else if (!wantAudible && mediaPlayer != null)
+ disableMediaPlayer();
+ } else if (PreferenceConstants.BELL_VOLUME.equals(key)) {
+ if (mediaPlayer != null) {
+ float volume = sharedPreferences.getFloat(
+ PreferenceConstants.BELL_VOLUME,
+ PreferenceConstants.DEFAULT_BELL_VOLUME);
+ mediaPlayer.setVolume(volume, volume);
+ }
+ } else if (PreferenceConstants.BELL_VIBRATE.equals(key)) {
+ wantBellVibration = sharedPreferences.getBoolean(
+ PreferenceConstants.BELL_VIBRATE, true);
+ } else if (PreferenceConstants.BUMPY_ARROWS.equals(key)) {
+ wantKeyVibration = sharedPreferences.getBoolean(
+ PreferenceConstants.BUMPY_ARROWS, true);
+ } else if (PreferenceConstants.WIFI_LOCK.equals(key)) {
+ final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true);
+ connectivityManager.setWantWifiLock(lockingWifi);
+ } else if (PreferenceConstants.MEMKEYS.equals(key)) {
+ updateSavingKeys();
+ }
+ }
+
+ /**
+ * Allow {@link TerminalBridge} to resize when the parent has changed.
+ * @param resizeAllowed
+ */
+ public void setResizeAllowed(boolean resizeAllowed) {
+ this.resizeAllowed = resizeAllowed;
+ }
+
+ public boolean isResizeAllowed() {
+ return resizeAllowed;
+ }
+
+ public static class KeyHolder {
+ public PubkeyBean bean;
+ public KeyPair pair;
+ public byte[] openSSHPubkey;
+ }
+
+ /**
+ * Called when connectivity to the network is lost and it doesn't appear
+ * we'll be getting a different connection any time soon.
+ */
+ public void onConnectivityLost() {
+ final Thread t = new Thread() {
+ @Override
+ public void run() {
+ disconnectAll(false);
+ }
+ };
+ t.setName("Disconnector");
+ t.start();
+ }
+
+ /**
+ * Called when connectivity to the network is restored.
+ */
+ public void onConnectivityRestored() {
+ final Thread t = new Thread() {
+ @Override
+ public void run() {
+ reconnectPending();
+ }
+ };
+ t.setName("Reconnector");
+ t.start();
+ }
+
+ /**
+ * Insert request into reconnect queue to be executed either immediately
+ * or later when connectivity is restored depending on whether we're
+ * currently connected.
+ *
+ * @param bridge the TerminalBridge to reconnect when possible
+ */
+ public void requestReconnect(TerminalBridge bridge) {
+ synchronized (mPendingReconnect) {
+ mPendingReconnect.add(new WeakReference<TerminalBridge>(bridge));
+ if (!bridge.isUsingNetwork() ||
+ connectivityManager.isConnected()) {
+ reconnectPending();
+ }
+ }
+ }
+
+ /**
+ * Reconnect all bridges that were pending a reconnect when connectivity
+ * was lost.
+ */
+ private void reconnectPending() {
+ synchronized (mPendingReconnect) {
+ for (WeakReference<TerminalBridge> ref : mPendingReconnect) {
+ TerminalBridge bridge = ref.get();
+ if (bridge == null) {
+ continue;
+ }
+ bridge.startConnection();
+ }
+ mPendingReconnect.clear();
+ }
+ }
+}