aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/org
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
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')
-rw-r--r--app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java423
-rw-r--r--app/src/main/java/org/connectbot/ActionBarWrapper.java83
-rw-r--r--app/src/main/java/org/connectbot/ColorsActivity.java331
-rw-r--r--app/src/main/java/org/connectbot/ConsoleActivity.java1175
-rw-r--r--app/src/main/java/org/connectbot/GeneratePubkeyActivity.java353
-rw-r--r--app/src/main/java/org/connectbot/HelpActivity.java77
-rw-r--r--app/src/main/java/org/connectbot/HelpTopicActivity.java49
-rw-r--r--app/src/main/java/org/connectbot/HostEditorActivity.java433
-rw-r--r--app/src/main/java/org/connectbot/HostListActivity.java570
-rw-r--r--app/src/main/java/org/connectbot/PortForwardListActivity.java425
-rw-r--r--app/src/main/java/org/connectbot/PubkeyListActivity.java673
-rw-r--r--app/src/main/java/org/connectbot/SettingsActivity.java60
-rw-r--r--app/src/main/java/org/connectbot/StrictModeSetup.java23
-rw-r--r--app/src/main/java/org/connectbot/TerminalView.java452
-rw-r--r--app/src/main/java/org/connectbot/WizardActivity.java105
-rw-r--r--app/src/main/java/org/connectbot/bean/AbstractBean.java49
-rw-r--r--app/src/main/java/org/connectbot/bean/HostBean.java317
-rw-r--r--app/src/main/java/org/connectbot/bean/PortForwardBean.java239
-rw-r--r--app/src/main/java/org/connectbot/bean/PubkeyBean.java234
-rw-r--r--app/src/main/java/org/connectbot/bean/SelectionArea.java201
-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
-rw-r--r--app/src/main/java/org/connectbot/transport/AbsTransport.java254
-rw-r--r--app/src/main/java/org/connectbot/transport/Local.java219
-rw-r--r--app/src/main/java/org/connectbot/transport/SSH.java958
-rw-r--r--app/src/main/java/org/connectbot/transport/Telnet.java330
-rw-r--r--app/src/main/java/org/connectbot/transport/TransportFactory.java132
-rw-r--r--app/src/main/java/org/connectbot/util/Colors.java91
-rw-r--r--app/src/main/java/org/connectbot/util/EastAsianWidth.java75
-rw-r--r--app/src/main/java/org/connectbot/util/Encryptor.java205
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyDialog.java50
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyView.java169
-rw-r--r--app/src/main/java/org/connectbot/util/HelpTopicView.java62
-rw-r--r--app/src/main/java/org/connectbot/util/HostDatabase.java766
-rw-r--r--app/src/main/java/org/connectbot/util/OnDbWrittenListener.java26
-rw-r--r--app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java22
-rw-r--r--app/src/main/java/org/connectbot/util/PreferenceConstants.java90
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyDatabase.java329
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyUtils.java352
-rw-r--r--app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java133
-rw-r--r--app/src/main/java/org/connectbot/util/UberColorPickerDialog.java982
-rw-r--r--app/src/main/java/org/connectbot/util/VolumePreference.java72
-rw-r--r--app/src/main/java/org/connectbot/util/XmlBuilder.java71
-rw-r--r--app/src/main/java/org/keyczar/jce/EcCore.java679
-rw-r--r--app/src/main/java/org/openintents/intents/FileManagerIntents.java66
55 files changed, 15640 insertions, 0 deletions
diff --git a/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java b/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java
new file mode 100644
index 0000000..d61ef59
--- /dev/null
+++ b/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.harmony.niochar.charset.additional;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+/* TODO: support direct byte buffers
+import org.apache.harmony.nio.AddressUtil;
+import org.apache.harmony.niochar.CharsetProviderImpl;
+*/
+
+public class IBM437 extends Charset {
+
+ public IBM437(String csName, String[] aliases) {
+ super(csName, aliases);
+ }
+
+ public boolean contains(Charset cs) {
+ return cs.name().equalsIgnoreCase("IBM367") || cs.name().equalsIgnoreCase("IBM437") || cs.name().equalsIgnoreCase("US-ASCII") ;
+ }
+
+ public CharsetDecoder newDecoder() {
+ return new Decoder(this);
+ }
+
+ public CharsetEncoder newEncoder() {
+ return new Encoder(this);
+ }
+
+ private static final class Decoder extends CharsetDecoder{
+ private Decoder(Charset cs){
+ super(cs, 1, 1);
+
+ }
+
+ private native int nDecode(char[] array, int arrPosition, int remaining, long outAddr, int absolutePos);
+
+
+ protected CoderResult decodeLoop(ByteBuffer bb, CharBuffer cb){
+ int cbRemaining = cb.remaining();
+/* TODO: support direct byte buffers
+ if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && bb.hasRemaining() && cb.hasArray()){
+ int toProceed = bb.remaining();
+ int cbPos = cb.position();
+ int bbPos = bb.position();
+ boolean throwOverflow = false;
+ if( cbRemaining < toProceed ) {
+ toProceed = cbRemaining;
+ throwOverflow = true;
+ }
+ int res = nDecode(cb.array(), cb.arrayOffset()+cbPos, toProceed, AddressUtil.getDirectBufferAddress(bb), bbPos);
+ bb.position(bbPos+res);
+ cb.position(cbPos+res);
+ if(throwOverflow) return CoderResult.OVERFLOW;
+ }else{
+*/
+ if(bb.hasArray() && cb.hasArray()) {
+ int rem = bb.remaining();
+ rem = cbRemaining >= rem ? rem : cbRemaining;
+ byte[] bArr = bb.array();
+ char[] cArr = cb.array();
+ int bStart = bb.position();
+ int cStart = cb.position();
+ int i;
+ for(i=bStart; i<bStart+rem; i++) {
+ char in = (char)(bArr[i] & 0xFF);
+ if(in >= 26){
+ int index = (int)in - 26;
+ cArr[cStart++] = (char)arr[index];
+ }else {
+ cArr[cStart++] = (char)(in & 0xFF);
+ }
+ }
+ bb.position(i);
+ cb.position(cStart);
+ if(rem == cbRemaining && bb.hasRemaining()) return CoderResult.OVERFLOW;
+ } else {
+ while(bb.hasRemaining()){
+ if( cbRemaining == 0 ) return CoderResult.OVERFLOW;
+ char in = (char)(bb.get() & 0xFF);
+ if(in >= 26){
+ int index = (int)in - 26;
+ cb.put(arr[index]);
+ }else {
+ cb.put((char)(in & 0xFF));
+ }
+ cbRemaining--;
+ }
+/*
+ }
+*/
+ }
+ return CoderResult.UNDERFLOW;
+ }
+
+ final static char[] arr = {
+ 0x001C,0x001B,0x007F,0x001D,0x001E,0x001F,
+ 0x0020,0x0021,0x0022,0x0023,0x0024,0x0025,0x0026,0x0027,
+ 0x0028,0x0029,0x002A,0x002B,0x002C,0x002D,0x002E,0x002F,
+ 0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,
+ 0x0038,0x0039,0x003A,0x003B,0x003C,0x003D,0x003E,0x003F,
+ 0x0040,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,
+ 0x0048,0x0049,0x004A,0x004B,0x004C,0x004D,0x004E,0x004F,
+ 0x0050,0x0051,0x0052,0x0053,0x0054,0x0055,0x0056,0x0057,
+ 0x0058,0x0059,0x005A,0x005B,0x005C,0x005D,0x005E,0x005F,
+ 0x0060,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,
+ 0x0068,0x0069,0x006A,0x006B,0x006C,0x006D,0x006E,0x006F,
+ 0x0070,0x0071,0x0072,0x0073,0x0074,0x0075,0x0076,0x0077,
+ 0x0078,0x0079,0x007A,0x007B,0x007C,0x007D,0x007E,0x001A,
+ 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
+ 0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
+ 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
+ 0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
+ 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
+ 0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
+ 0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
+ 0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
+ 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
+ 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
+ 0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
+ 0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
+ 0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x03BC,0x03C4,
+ 0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
+ 0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
+ 0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0
+ };
+ }
+
+ private static final class Encoder extends CharsetEncoder{
+ private Encoder(Charset cs){
+ super(cs, 1, 1);
+ }
+
+ private native void nEncode(long outAddr, int absolutePos, char[] array, int arrPosition, int[] res);
+
+ protected CoderResult encodeLoop(CharBuffer cb, ByteBuffer bb){
+ int bbRemaining = bb.remaining();
+/* TODO: support direct byte buffers
+ if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && cb.hasRemaining() && cb.hasArray()){
+ int toProceed = cb.remaining();
+ int cbPos = cb.position();
+ int bbPos = bb.position();
+ boolean throwOverflow = false;
+ if( bbRemaining < toProceed ) {
+ toProceed = bbRemaining;
+ throwOverflow = true;
+ }
+ int[] res = {toProceed, 0};
+ nEncode(AddressUtil.getDirectBufferAddress(bb), bbPos, cb.array(), cb.arrayOffset()+cbPos, res);
+ if( res[0] <= 0 ) {
+ bb.position(bbPos-res[0]);
+ cb.position(cbPos-res[0]);
+ if(res[1]!=0) {
+ if(res[1] < 0)
+ return CoderResult.malformedForLength(-res[1]);
+ else
+ return CoderResult.unmappableForLength(res[1]);
+ }
+ }else{
+ bb.position(bbPos+res[0]);
+ cb.position(cbPos+res[0]);
+ if(throwOverflow) return CoderResult.OVERFLOW;
+ }
+ }else{
+*/
+ if(bb.hasArray() && cb.hasArray()) {
+ byte[] byteArr = bb.array();
+ char[] charArr = cb.array();
+ int rem = cb.remaining();
+ int byteArrStart = bb.position();
+ rem = bbRemaining <= rem ? bbRemaining : rem;
+ int x;
+ for(x = cb.position(); x < cb.position()+rem; x++) {
+ char c = charArr[x];
+ if(c > (char)0x25A0){
+ if (c >= 0xD800 && c <= 0xDFFF) {
+ if(x+1 < cb.limit()) {
+ char c1 = charArr[x+1];
+ if(c1 >= 0xD800 && c1 <= 0xDFFF) {
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.unmappableForLength(2);
+ }
+ } else {
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.UNDERFLOW;
+ }
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.malformedForLength(1);
+ }
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.unmappableForLength(1);
+ }else{
+ if(c < 0x1A) {
+ byteArr[byteArrStart++] = (byte)c;
+ } else {
+ int index = (int)c >> 8;
+ index = encodeIndex[index];
+ if(index < 0) {
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.unmappableForLength(1);
+ }
+ index <<= 8;
+ index += (int)c & 0xFF;
+ if((byte)arr[index] != 0){
+ byteArr[byteArrStart++] = (byte)arr[index];
+ }else{
+ cb.position(x); bb.position(byteArrStart);
+ return CoderResult.unmappableForLength(1);
+ }
+ }
+ }
+ }
+ cb.position(x);
+ bb.position(byteArrStart);
+ if(rem == bbRemaining && cb.hasRemaining()) {
+ return CoderResult.OVERFLOW;
+ }
+ } else {
+ while(cb.hasRemaining()){
+ if( bbRemaining == 0 ) return CoderResult.OVERFLOW;
+ char c = cb.get();
+ if(c > (char)0x25A0){
+ if (c >= 0xD800 && c <= 0xDFFF) {
+ if(cb.hasRemaining()) {
+ char c1 = cb.get();
+ if(c1 >= 0xD800 && c1 <= 0xDFFF) {
+ cb.position(cb.position()-2);
+ return CoderResult.unmappableForLength(2);
+ } else {
+ cb.position(cb.position()-1);
+ }
+ } else {
+ cb.position(cb.position()-1);
+ return CoderResult.UNDERFLOW;
+ }
+ cb.position(cb.position()-1);
+ return CoderResult.malformedForLength(1);
+ }
+ cb.position(cb.position()-1);
+ return CoderResult.unmappableForLength(1);
+ }else{
+ if(c < 0x1A) {
+ bb.put((byte)c);
+ } else {
+ int index = (int)c >> 8;
+ index = encodeIndex[index];
+ if(index < 0) {
+ cb.position(cb.position()-1);
+ return CoderResult.unmappableForLength(1);
+ }
+ index <<= 8;
+ index += (int)c & 0xFF;
+ if((byte)arr[index] != 0){
+ bb.put((byte)arr[index]);
+ }else{
+ cb.position(cb.position()-1);
+ return CoderResult.unmappableForLength(1);
+ }
+ }
+ bbRemaining--;
+ }
+ }
+/* TODO: support direct byte buffers
+ }
+*/
+ }
+ return CoderResult.UNDERFLOW;
+ }
+
+ final static char arr[] = {
+
+ 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,
+ 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x7F,0x1B,0x1A,0x1D,0x1E,0x1F,
+ 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,
+ 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
+ 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,
+ 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,
+ 0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
+ 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x1C,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xFF,0xAD,0x9B,0x9C,0x00,0x9D,0x00,0x00,0x00,0x00,0xA6,0xAE,0xAA,0x00,0x00,0x00,
+ 0xF8,0xF1,0xFD,0x00,0x00,0x00,0x00,0xFA,0x00,0x00,0xA7,0xAF,0xAC,0xAB,0x00,0xA8,
+ 0x00,0x00,0x00,0x00,0x8E,0x8F,0x92,0x80,0x00,0x90,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0xA5,0x00,0x00,0x00,0x00,0x99,0x00,0x00,0x00,0x00,0x00,0x9A,0x00,0x00,0xE1,
+ 0x85,0xA0,0x83,0x00,0x84,0x86,0x91,0x87,0x8A,0x82,0x88,0x89,0x8D,0xA1,0x8C,0x8B,
+ 0x00,0xA4,0x95,0xA2,0x93,0x00,0x94,0xF6,0x00,0x97,0xA3,0x96,0x81,0x00,0x00,0x98,
+
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x9F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0xE2,0x00,0x00,0x00,0x00,0xE9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0xE4,0x00,0x00,0xE8,0x00,0x00,0xEA,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0xE0,0x00,0x00,0xEB,0xEE,0x00,0x00,0x00,0x00,0x00,0x00,0xE6,0x00,0x00,0x00,
+ 0xE3,0x00,0x00,0xE5,0xE7,0x00,0xED,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF9,0xFB,0x00,0x00,0x00,0xEC,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEF,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0xF0,0x00,0x00,0xF3,0xF2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xA9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xF4,0xF5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+ 0xC4,0x00,0xB3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xDA,0x00,0x00,0x00,
+ 0xBF,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xD9,0x00,0x00,0x00,0xC3,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0xB4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC2,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0xC1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC5,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xCD,0xBA,0xD5,0xD6,0xC9,0xB8,0xB7,0xBB,0xD4,0xD3,0xC8,0xBE,0xBD,0xBC,0xC6,0xC7,
+ 0xCC,0xB5,0xB6,0xB9,0xD1,0xD2,0xCB,0xCF,0xD0,0xCA,0xD8,0xD7,0xCE,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xDF,0x00,0x00,0x00,0xDC,0x00,0x00,0x00,0xDB,0x00,0x00,0x00,0xDD,0x00,0x00,0x00,
+ 0xDE,0xB0,0xB1,0xB2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
+ };
+
+ final static int[] encodeIndex = {
+ 0,1,-1,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ 3,-1,4,5,-1,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ };
+ }
+}
diff --git a/app/src/main/java/org/connectbot/ActionBarWrapper.java b/app/src/main/java/org/connectbot/ActionBarWrapper.java
new file mode 100644
index 0000000..0c7b65d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/ActionBarWrapper.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import org.connectbot.util.PreferenceConstants;
+
+import android.app.Activity;
+import android.app.ActionBar;
+
+public abstract class ActionBarWrapper {
+ public interface OnMenuVisibilityListener {
+ public void onMenuVisibilityChanged(boolean isVisible);
+ }
+
+ public static ActionBarWrapper getActionBar(Activity activity) {
+ if (PreferenceConstants.PRE_HONEYCOMB)
+ return new DummyActionBar();
+ else
+ return new RealActionBar(activity);
+ }
+
+ public void hide() {
+ }
+
+ public void show() {
+ }
+
+ public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+ }
+
+ public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
+ }
+
+ private static class DummyActionBar extends ActionBarWrapper {
+ }
+
+ private static class RealActionBar extends ActionBarWrapper {
+ private final ActionBar actionBar;
+
+ public RealActionBar(Activity activity) {
+ actionBar = activity.getActionBar();
+ }
+
+ @Override
+ public void hide() {
+ actionBar.hide();
+ }
+
+ @Override
+ public void show() {
+ actionBar.show();
+ }
+
+ @Override
+ public void addOnMenuVisibilityListener(final OnMenuVisibilityListener listener) {
+ actionBar.addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ listener.onMenuVisibilityChanged(isVisible);
+ }
+ });
+ }
+
+ @Override
+ public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
+ actionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/ColorsActivity.java b/app/src/main/java/org/connectbot/ColorsActivity.java
new file mode 100644
index 0000000..38336f7
--- /dev/null
+++ b/app/src/main/java/org/connectbot/ColorsActivity.java
@@ -0,0 +1,331 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.connectbot.util.Colors;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.UberColorPickerDialog;
+import org.connectbot.util.UberColorPickerDialog.OnColorChangedListener;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.Spinner;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class ColorsActivity extends Activity implements OnItemClickListener, OnColorChangedListener, OnItemSelectedListener {
+ private GridView mColorGrid;
+ private Spinner mFgSpinner;
+ private Spinner mBgSpinner;
+
+ private int mColorScheme;
+
+ private List<Integer> mColorList;
+ private HostDatabase hostdb;
+
+ private int mCurrentColor = 0;
+
+ private int[] mDefaultColors;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.act_colors);
+
+ this.setTitle(String.format("%s: %s",
+ getResources().getText(R.string.app_name),
+ getResources().getText(R.string.title_colors)));
+
+ mColorScheme = HostDatabase.DEFAULT_COLOR_SCHEME;
+
+ hostdb = new HostDatabase(this);
+
+ mColorList = Arrays.asList(hostdb.getColorsForScheme(mColorScheme));
+ mDefaultColors = hostdb.getDefaultColorsForScheme(mColorScheme);
+
+ mColorGrid = (GridView) findViewById(R.id.color_grid);
+ mColorGrid.setAdapter(new ColorsAdapter(true));
+ mColorGrid.setOnItemClickListener(this);
+ mColorGrid.setSelection(0);
+
+ mFgSpinner = (Spinner) findViewById(R.id.fg);
+ mFgSpinner.setAdapter(new ColorsAdapter(false));
+ mFgSpinner.setSelection(mDefaultColors[0]);
+ mFgSpinner.setOnItemSelectedListener(this);
+
+ mBgSpinner = (Spinner) findViewById(R.id.bg);
+ mBgSpinner.setAdapter(new ColorsAdapter(false));
+ mBgSpinner.setSelection(mDefaultColors[1]);
+ mBgSpinner.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (hostdb != null) {
+ hostdb.close();
+ hostdb = null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (hostdb == null)
+ hostdb = new HostDatabase(this);
+ }
+
+ private class ColorsAdapter extends BaseAdapter {
+ private boolean mSquareViews;
+
+ public ColorsAdapter(boolean squareViews) {
+ mSquareViews = squareViews;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ColorView c;
+
+ if (convertView == null) {
+ c = new ColorView(ColorsActivity.this, mSquareViews);
+ } else {
+ c = (ColorView) convertView;
+ }
+
+ c.setColor(mColorList.get(position));
+ c.setNumber(position + 1);
+
+ return c;
+ }
+
+ public int getCount() {
+ return mColorList.size();
+ }
+
+ public Object getItem(int position) {
+ return mColorList.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+ }
+
+ private class ColorView extends View {
+ private boolean mSquare;
+
+ private Paint mTextPaint;
+ private Paint mShadowPaint;
+
+ // Things we paint
+ private int mBackgroundColor;
+ private String mText;
+
+ private int mAscent;
+ private int mWidthCenter;
+ private int mHeightCenter;
+
+ public ColorView(Context context, boolean square) {
+ super(context);
+
+ mSquare = square;
+
+ mTextPaint = new Paint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setTextSize(16);
+ mTextPaint.setColor(0xFFFFFFFF);
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+
+ mShadowPaint = new Paint(mTextPaint);
+ mShadowPaint.setStyle(Paint.Style.STROKE);
+ mShadowPaint.setStrokeCap(Paint.Cap.ROUND);
+ mShadowPaint.setStrokeJoin(Paint.Join.ROUND);
+ mShadowPaint.setStrokeWidth(4f);
+ mShadowPaint.setColor(0xFF000000);
+
+ setPadding(10, 10, 10, 10);
+ }
+
+ public void setColor(int color) {
+ mBackgroundColor = color;
+ }
+
+ public void setNumber(int number) {
+ mText = Integer.toString(number);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = measureWidth(widthMeasureSpec);
+
+ int height;
+ if (mSquare)
+ height = width;
+ else
+ height = measureHeight(heightMeasureSpec);
+
+ mAscent = (int) mTextPaint.ascent();
+ mWidthCenter = width / 2;
+ mHeightCenter = height / 2 - mAscent / 2;
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int measureWidth(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY) {
+ // We were told how big to be
+ result = specSize;
+ } else {
+ // Measure the text
+ result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
+ + getPaddingRight();
+ if (specMode == MeasureSpec.AT_MOST) {
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ result = Math.min(result, specSize);
+ }
+ }
+
+ return result;
+ }
+
+ private int measureHeight(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ mAscent = (int) mTextPaint.ascent();
+ if (specMode == MeasureSpec.EXACTLY) {
+ // We were told how big to be
+ result = specSize;
+ } else {
+ // Measure the text (beware: ascent is a negative number)
+ result = (int) (-mAscent + mTextPaint.descent())
+ + getPaddingTop() + getPaddingBottom();
+ if (specMode == MeasureSpec.AT_MOST) {
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ canvas.drawColor(mBackgroundColor);
+
+ canvas.drawText(mText, mWidthCenter, mHeightCenter, mShadowPaint);
+ canvas.drawText(mText, mWidthCenter, mHeightCenter, mTextPaint);
+ }
+ }
+
+ private void editColor(int colorNumber) {
+ mCurrentColor = colorNumber;
+ new UberColorPickerDialog(this, this, mColorList.get(colorNumber)).show();
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ editColor(position);
+ }
+
+ public void onNothingSelected(AdapterView<?> arg0) { }
+
+ public void colorChanged(int value) {
+ hostdb.setGlobalColor(mCurrentColor, value);
+ mColorList.set(mCurrentColor, value);
+ mColorGrid.invalidateViews();
+ }
+
+ public void onItemSelected(AdapterView<?> parent, View view, int position,
+ long id) {
+ boolean needUpdate = false;
+ if (parent == mFgSpinner) {
+ if (position != mDefaultColors[0]) {
+ mDefaultColors[0] = position;
+ needUpdate = true;
+ }
+ } else if (parent == mBgSpinner) {
+ if (position != mDefaultColors[1]) {
+ mDefaultColors[1] = position;
+ needUpdate = true;
+ }
+ }
+
+ if (needUpdate)
+ hostdb.setDefaultColorsForScheme(mColorScheme, mDefaultColors[0], mDefaultColors[1]);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem reset = menu.add(R.string.menu_colors_reset);
+ reset.setAlphabeticShortcut('r');
+ reset.setNumericShortcut('1');
+ reset.setIcon(android.R.drawable.ic_menu_revert);
+ reset.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem arg0) {
+ // Reset each individual color to defaults.
+ for (int i = 0; i < Colors.defaults.length; i++) {
+ if (mColorList.get(i) != Colors.defaults[i]) {
+ hostdb.setGlobalColor(i, Colors.defaults[i]);
+ mColorList.set(i, Colors.defaults[i]);
+ }
+ }
+ mColorGrid.invalidateViews();
+
+ // Reset the default FG/BG colors as well.
+ mFgSpinner.setSelection(HostDatabase.DEFAULT_FG_COLOR);
+ mBgSpinner.setSelection(HostDatabase.DEFAULT_BG_COLOR);
+ hostdb.setDefaultColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME,
+ HostDatabase.DEFAULT_FG_COLOR, HostDatabase.DEFAULT_BG_COLOR);
+
+ return true;
+ }
+ });
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java
new file mode 100644
index 0000000..2359bea
--- /dev/null
+++ b/app/src/main/java/org/connectbot/ConsoleActivity.java
@@ -0,0 +1,1175 @@
+/*
+ * 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;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+import org.connectbot.bean.SelectionArea;
+import org.connectbot.service.PromptHelper;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalKeyListener;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.PreferenceConstants;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.View.OnTouchListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+import android.widget.AdapterView.OnItemClickListener;
+
+import de.mud.terminal.vt320;
+
+public class ConsoleActivity extends Activity {
+ public final static String TAG = "ConnectBot.ConsoleActivity";
+
+ protected static final int REQUEST_EDIT = 1;
+
+ private static final int CLICK_TIME = 400;
+ private static final float MAX_CLICK_DISTANCE = 25f;
+ private static final int KEYBOARD_DISPLAY_TIME = 1500;
+
+ // Direction to shift the ViewFlipper
+ private static final int SHIFT_LEFT = 0;
+ private static final int SHIFT_RIGHT = 1;
+
+ protected ViewFlipper flip = null;
+ protected TerminalManager bound = null;
+ protected LayoutInflater inflater = null;
+
+ private SharedPreferences prefs = null;
+
+ // determines whether or not menuitem accelerators are bound
+ // otherwise they collide with an external keyboard's CTRL-char
+ private boolean hardKeyboard = false;
+
+ protected Uri requested;
+
+ protected ClipboardManager clipboard;
+ private RelativeLayout stringPromptGroup;
+ protected EditText stringPrompt;
+ private TextView stringPromptInstructions;
+
+ private RelativeLayout booleanPromptGroup;
+ private TextView booleanPrompt;
+ private Button booleanYes, booleanNo;
+
+ private RelativeLayout keyboardGroup;
+ private Runnable keyboardGroupHider;
+
+ private TextView empty;
+
+ private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, fade_stay_hidden, fade_out_delayed;
+
+ private Animation keyboard_fade_in, keyboard_fade_out;
+ private float lastX, lastY;
+
+ private InputMethodManager inputManager;
+
+ private MenuItem disconnect, copy, paste, portForward, resize, urlscan;
+
+ protected TerminalBridge copySource = null;
+ private int lastTouchRow, lastTouchCol;
+
+ private boolean forcedOrientation;
+
+ private Handler handler = new Handler();
+
+ private ImageView mKeyboardButton;
+
+ private ActionBarWrapper actionBar;
+ private boolean inActionBarMenu = false;
+
+ private ServiceConnection connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ bound = ((TerminalManager.TerminalBinder) service).getService();
+
+ // let manager know about our event handling services
+ bound.disconnectHandler = disconnectHandler;
+
+ Log.d(TAG, String.format("Connected to TerminalManager and found bridges.size=%d", bound.bridges.size()));
+
+ bound.setResizeAllowed(true);
+
+ // clear out any existing bridges and record requested index
+ flip.removeAllViews();
+
+ final String requestedNickname = (requested != null) ? requested.getFragment() : null;
+ int requestedIndex = 0;
+
+ TerminalBridge requestedBridge = bound.getConnectedBridge(requestedNickname);
+
+ // If we didn't find the requested connection, try opening it
+ if (requestedNickname != null && requestedBridge == null) {
+ try {
+ Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now", requested.toString(), requestedNickname));
+ requestedBridge = bound.openConnection(requested);
+ } catch(Exception e) {
+ Log.e(TAG, "Problem while trying to create new requested bridge from URI", e);
+ }
+ }
+
+ // create views for all bridges on this service
+ for (TerminalBridge bridge : bound.bridges) {
+
+ final int currentIndex = addNewTerminalView(bridge);
+
+ // check to see if this bridge was requested
+ if (bridge == requestedBridge)
+ requestedIndex = currentIndex;
+ }
+
+ setDisplayedTerminal(requestedIndex);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ // tell each bridge to forget about our prompt handler
+ synchronized (bound.bridges) {
+ for(TerminalBridge bridge : bound.bridges)
+ bridge.promptHelper.setHandler(null);
+ }
+
+ flip.removeAllViews();
+ updateEmptyVisible();
+ bound = null;
+ }
+ };
+
+ protected Handler promptHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ // someone below us requested to display a prompt
+ updatePromptVisible();
+ }
+ };
+
+ protected Handler disconnectHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Log.d(TAG, "Someone sending HANDLE_DISCONNECT to parentHandler");
+
+ // someone below us requested to display a password dialog
+ // they are sending nickname and requested
+ TerminalBridge bridge = (TerminalBridge)msg.obj;
+
+ if (bridge.isAwaitingClose())
+ closeBridge(bridge);
+ }
+ };
+
+ /**
+ * @param bridge
+ */
+ private void closeBridge(final TerminalBridge bridge) {
+ synchronized (flip) {
+ final int flipIndex = getFlipIndex(bridge);
+
+ if (flipIndex >= 0) {
+ if (flip.getDisplayedChild() == flipIndex) {
+ shiftCurrentTerminal(SHIFT_LEFT);
+ }
+ flip.removeViewAt(flipIndex);
+
+ /* TODO Remove this workaround when ViewFlipper is fixed to listen
+ * to view removals. Android Issue 1784
+ */
+ final int numChildren = flip.getChildCount();
+ if (flip.getDisplayedChild() >= numChildren &&
+ numChildren > 0) {
+ flip.setDisplayedChild(numChildren - 1);
+ }
+
+ updateEmptyVisible();
+ }
+
+ // If we just closed the last bridge, go back to the previous activity.
+ if (flip.getChildCount() == 0) {
+ finish();
+ }
+ }
+ }
+
+ protected View findCurrentView(int id) {
+ View view = flip.getCurrentView();
+ if(view == null) return null;
+ return view.findViewById(id);
+ }
+
+ protected PromptHelper getCurrentPromptHelper() {
+ View view = findCurrentView(R.id.console_flip);
+ if(!(view instanceof TerminalView)) return null;
+ return ((TerminalView)view).bridge.promptHelper;
+ }
+
+ protected void hideAllPrompts() {
+ stringPromptGroup.setVisibility(View.GONE);
+ booleanPromptGroup.setVisibility(View.GONE);
+ }
+
+ private void showEmulatedKeys() {
+ keyboardGroup.startAnimation(keyboard_fade_in);
+ keyboardGroup.setVisibility(View.VISIBLE);
+ actionBar.show();
+
+ if (keyboardGroupHider != null)
+ handler.removeCallbacks(keyboardGroupHider);
+ keyboardGroupHider = new Runnable() {
+ public void run() {
+ if (keyboardGroup.getVisibility() == View.GONE || inActionBarMenu)
+ return;
+
+ keyboardGroup.startAnimation(keyboard_fade_out);
+ keyboardGroup.setVisibility(View.GONE);
+ actionBar.hide();
+ keyboardGroupHider = null;
+ }
+ };
+ handler.postDelayed(keyboardGroupHider, KEYBOARD_DISPLAY_TIME);
+ }
+
+ private void hideEmulatedKeys() {
+ if (keyboardGroupHider != null)
+ handler.removeCallbacks(keyboardGroupHider);
+ keyboardGroup.setVisibility(View.GONE);
+ actionBar.hide();
+ }
+
+ // more like configureLaxMode -- enable network IO on UI thread
+ private void configureStrictMode() {
+ try {
+ Class.forName("android.os.StrictMode");
+ StrictModeSetup.run();
+ } catch (ClassNotFoundException e) {
+ }
+ }
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ configureStrictMode();
+ hardKeyboard = getResources().getConfiguration().keyboard ==
+ Configuration.KEYBOARD_QWERTY;
+
+ this.setContentView(R.layout.act_console);
+
+ clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
+ prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // hide status bar if requested by user
+ if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) {
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ // TODO find proper way to disable volume key beep if it exists.
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // handle requested console from incoming intent
+ requested = getIntent().getData();
+
+ inflater = LayoutInflater.from(this);
+
+ flip = (ViewFlipper)findViewById(R.id.console_flip);
+ empty = (TextView)findViewById(android.R.id.empty);
+
+ stringPromptGroup = (RelativeLayout) findViewById(R.id.console_password_group);
+ stringPromptInstructions = (TextView) findViewById(R.id.console_password_instructions);
+ stringPrompt = (EditText)findViewById(R.id.console_password);
+ stringPrompt.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if(event.getAction() == KeyEvent.ACTION_UP) return false;
+ if(keyCode != KeyEvent.KEYCODE_ENTER) return false;
+
+ // pass collected password down to current terminal
+ String value = stringPrompt.getText().toString();
+
+ PromptHelper helper = getCurrentPromptHelper();
+ if(helper == null) return false;
+ helper.setResponse(value);
+
+ // finally clear password for next user
+ stringPrompt.setText("");
+ updatePromptVisible();
+
+ return true;
+ }
+ });
+
+ booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
+ booleanPrompt = (TextView)findViewById(R.id.console_prompt);
+
+ booleanYes = (Button)findViewById(R.id.console_prompt_yes);
+ booleanYes.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ PromptHelper helper = getCurrentPromptHelper();
+ if(helper == null) return;
+ helper.setResponse(Boolean.TRUE);
+ updatePromptVisible();
+ }
+ });
+
+ booleanNo = (Button)findViewById(R.id.console_prompt_no);
+ booleanNo.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ PromptHelper helper = getCurrentPromptHelper();
+ if(helper == null) return;
+ helper.setResponse(Boolean.FALSE);
+ updatePromptVisible();
+ }
+ });
+
+ // preload animations for terminal switching
+ slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
+ slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
+ slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
+ slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);
+
+ fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);
+ fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden);
+
+ // Preload animation for keyboard button
+ keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in);
+ keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out);
+
+ inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ keyboardGroup = (RelativeLayout) findViewById(R.id.keyboard_group);
+
+ mKeyboardButton = (ImageView) findViewById(R.id.button_keyboard);
+ mKeyboardButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ View flip = findCurrentView(R.id.console_flip);
+ if (flip == null)
+ return;
+
+ inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED);
+ hideEmulatedKeys();
+ }
+ });
+
+ final ImageView ctrlButton = (ImageView) findViewById(R.id.button_ctrl);
+ ctrlButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ View flip = findCurrentView(R.id.console_flip);
+ if (flip == null) return;
+ TerminalView terminal = (TerminalView)flip;
+
+ TerminalKeyListener handler = terminal.bridge.getKeyHandler();
+ handler.metaPress(TerminalKeyListener.OUR_CTRL_ON);
+ hideEmulatedKeys();
+ }
+ });
+
+ final ImageView escButton = (ImageView) findViewById(R.id.button_esc);
+ escButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ View flip = findCurrentView(R.id.console_flip);
+ if (flip == null) return;
+ TerminalView terminal = (TerminalView)flip;
+
+ TerminalKeyListener handler = terminal.bridge.getKeyHandler();
+ handler.sendEscape();
+ hideEmulatedKeys();
+ }
+ });
+
+ actionBar = ActionBarWrapper.getActionBar(this);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.hide();
+ actionBar.addOnMenuVisibilityListener(new ActionBarWrapper.OnMenuVisibilityListener() {
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ inActionBarMenu = isVisible;
+ if (isVisible == false) {
+ hideEmulatedKeys();
+ }
+ }
+ });
+
+ // detect fling gestures to switch between terminals
+ final GestureDetector detect = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+ private float totalY = 0;
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+
+ final float distx = e2.getRawX() - e1.getRawX();
+ final float disty = e2.getRawY() - e1.getRawY();
+ final int goalwidth = flip.getWidth() / 2;
+
+ // need to slide across half of display to trigger console change
+ // make sure user kept a steady hand horizontally
+ if (Math.abs(disty) < (flip.getHeight() / 4)) {
+ if (distx > goalwidth) {
+ shiftCurrentTerminal(SHIFT_RIGHT);
+ return true;
+ }
+
+ if (distx < -goalwidth) {
+ shiftCurrentTerminal(SHIFT_LEFT);
+ return true;
+ }
+
+ }
+
+ return false;
+ }
+
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+ // if copying, then ignore
+ if (copySource != null && copySource.isSelectingForCopy())
+ return false;
+
+ if (e1 == null || e2 == null)
+ return false;
+
+ // if releasing then reset total scroll
+ if (e2.getAction() == MotionEvent.ACTION_UP) {
+ totalY = 0;
+ }
+
+ // activate consider if within x tolerance
+ if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) {
+
+ View flip = findCurrentView(R.id.console_flip);
+ if(flip == null) return false;
+ TerminalView terminal = (TerminalView)flip;
+
+ // estimate how many rows we have scrolled through
+ // accumulate distance that doesn't trigger immediate scroll
+ totalY += distanceY;
+ final int moved = (int)(totalY / terminal.bridge.charHeight);
+
+ // consume as scrollback only if towards right half of screen
+ if (e2.getX() > flip.getWidth() / 2) {
+ if (moved != 0) {
+ int base = terminal.bridge.buffer.getWindowBase();
+ terminal.bridge.buffer.setWindowBase(base + moved);
+ totalY = 0;
+ return true;
+ }
+ } else {
+ // otherwise consume as pgup/pgdown for every 5 lines
+ if (moved > 5) {
+ ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
+ terminal.bridge.tryKeyVibrate();
+ totalY = 0;
+ return true;
+ } else if (moved < -5) {
+ ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
+ terminal.bridge.tryKeyVibrate();
+ totalY = 0;
+ return true;
+ }
+
+ }
+
+ }
+
+ return false;
+ }
+
+
+ });
+
+ flip.setLongClickable(true);
+ flip.setOnTouchListener(new OnTouchListener() {
+
+ public boolean onTouch(View v, MotionEvent event) {
+
+ // when copying, highlight the area
+ if (copySource != null && copySource.isSelectingForCopy()) {
+ int row = (int)Math.floor(event.getY() / copySource.charHeight);
+ int col = (int)Math.floor(event.getX() / copySource.charWidth);
+
+ SelectionArea area = copySource.getSelectionArea();
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // recording starting area
+ if (area.isSelectingOrigin()) {
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchRow = row;
+ lastTouchCol = col;
+ copySource.redraw();
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ /* ignore when user hasn't moved since last time so
+ * we can fine-tune with directional pad
+ */
+ if (row == lastTouchRow && col == lastTouchCol)
+ return true;
+
+ // if the user moves, start the selection for other corner
+ area.finishSelectingOrigin();
+
+ // update selected area
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchRow = row;
+ lastTouchCol = col;
+ copySource.redraw();
+ return true;
+ case MotionEvent.ACTION_UP:
+ /* If they didn't move their finger, maybe they meant to
+ * select the rest of the text with the directional pad.
+ */
+ if (area.getLeft() == area.getRight() &&
+ area.getTop() == area.getBottom()) {
+ return true;
+ }
+
+ // copy selected area to clipboard
+ String copiedText = area.copyFrom(copySource.buffer);
+
+ clipboard.setText(copiedText);
+ Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show();
+ // fall through to clear state
+
+ case MotionEvent.ACTION_CANCEL:
+ // make sure we clear any highlighted area
+ area.reset();
+ copySource.setSelectingForCopy(false);
+ copySource.redraw();
+ return true;
+ }
+ }
+
+ Configuration config = getResources().getConfiguration();
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ lastX = event.getX();
+ lastY = event.getY();
+ } else if (event.getAction() == MotionEvent.ACTION_UP
+ && keyboardGroup.getVisibility() == View.GONE
+ && event.getEventTime() - event.getDownTime() < CLICK_TIME
+ && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
+ && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
+ showEmulatedKeys();
+ }
+
+ // pass any touch events back to detector
+ return detect.onTouchEvent(event);
+ }
+
+ });
+
+ }
+
+ /**
+ *
+ */
+ private void configureOrientation() {
+ String rotateDefault;
+ if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS)
+ rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
+ else
+ rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;
+
+ String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
+ if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate))
+ rotate = rotateDefault;
+
+ // request a forced orientation if requested by user
+ if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ forcedOrientation = true;
+ } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ forcedOrientation = true;
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ forcedOrientation = false;
+ }
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ View view = findCurrentView(R.id.console_flip);
+ final boolean activeTerminal = (view instanceof TerminalView);
+ boolean sessionOpen = false;
+ boolean disconnected = false;
+ boolean canForwardPorts = false;
+
+ if (activeTerminal) {
+ TerminalBridge bridge = ((TerminalView) view).bridge;
+ sessionOpen = bridge.isSessionOpen();
+ disconnected = bridge.isDisconnected();
+ canForwardPorts = bridge.canFowardPorts();
+ }
+
+ menu.setQwertyMode(true);
+
+ disconnect = menu.add(R.string.list_host_disconnect);
+ if (hardKeyboard)
+ disconnect.setAlphabeticShortcut('w');
+ if (!sessionOpen && disconnected)
+ disconnect.setTitle(R.string.console_menu_close);
+ disconnect.setEnabled(activeTerminal);
+ disconnect.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ disconnect.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // disconnect or close the currently visible session
+ TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+ TerminalBridge bridge = terminalView.bridge;
+
+ bridge.dispatchDisconnect(true);
+ return true;
+ }
+ });
+
+ copy = menu.add(R.string.console_menu_copy);
+ if (hardKeyboard)
+ copy.setAlphabeticShortcut('c');
+ copy.setIcon(android.R.drawable.ic_menu_set_as);
+ copy.setEnabled(activeTerminal);
+ copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // mark as copying and reset any previous bounds
+ TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+ copySource = terminalView.bridge;
+
+ SelectionArea area = copySource.getSelectionArea();
+ area.reset();
+ area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows());
+
+ copySource.setSelectingForCopy(true);
+
+ // Make sure we show the initial selection
+ copySource.redraw();
+
+ Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
+ return true;
+ }
+ });
+
+ paste = menu.add(R.string.console_menu_paste);
+ if (hardKeyboard)
+ paste.setAlphabeticShortcut('v');
+ paste.setIcon(android.R.drawable.ic_menu_edit);
+ paste.setEnabled(clipboard.hasText() && sessionOpen);
+ paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // force insert of clipboard text into current console
+ TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+ TerminalBridge bridge = terminalView.bridge;
+
+ // pull string from clipboard and generate all events to force down
+ String clip = clipboard.getText().toString();
+ bridge.injectString(clip);
+
+ return true;
+ }
+ });
+
+ portForward = menu.add(R.string.console_menu_portforwards);
+ if (hardKeyboard)
+ portForward.setAlphabeticShortcut('f');
+ portForward.setIcon(android.R.drawable.ic_menu_manage);
+ portForward.setEnabled(sessionOpen && canForwardPorts);
+ portForward.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+ TerminalBridge bridge = terminalView.bridge;
+
+ Intent intent = new Intent(ConsoleActivity.this, PortForwardListActivity.class);
+ intent.putExtra(Intent.EXTRA_TITLE, bridge.host.getId());
+ ConsoleActivity.this.startActivityForResult(intent, REQUEST_EDIT);
+ return true;
+ }
+ });
+
+ urlscan = menu.add(R.string.console_menu_urlscan);
+ if (hardKeyboard)
+ urlscan.setAlphabeticShortcut('u');
+ urlscan.setIcon(android.R.drawable.ic_menu_search);
+ urlscan.setEnabled(activeTerminal);
+ urlscan.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+
+ List<String> urls = terminalView.bridge.scanForURLs();
+
+ Dialog urlDialog = new Dialog(ConsoleActivity.this);
+ urlDialog.setTitle(R.string.console_menu_urlscan);
+
+ ListView urlListView = new ListView(ConsoleActivity.this);
+ URLItemListener urlListener = new URLItemListener(ConsoleActivity.this);
+ urlListView.setOnItemClickListener(urlListener);
+
+ urlListView.setAdapter(new ArrayAdapter<String>(ConsoleActivity.this, android.R.layout.simple_list_item_1, urls));
+ urlDialog.setContentView(urlListView);
+ urlDialog.show();
+
+ return true;
+ }
+ });
+
+ resize = menu.add(R.string.console_menu_resize);
+ if (hardKeyboard)
+ resize.setAlphabeticShortcut('s');
+ resize.setIcon(android.R.drawable.ic_menu_crop);
+ resize.setEnabled(sessionOpen);
+ resize.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+
+ final View resizeView = inflater.inflate(R.layout.dia_resize, null, false);
+ new AlertDialog.Builder(ConsoleActivity.this)
+ .setView(resizeView)
+ .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ int width, height;
+ try {
+ width = Integer.parseInt(((EditText) resizeView
+ .findViewById(R.id.width))
+ .getText().toString());
+ height = Integer.parseInt(((EditText) resizeView
+ .findViewById(R.id.height))
+ .getText().toString());
+ } catch (NumberFormatException nfe) {
+ // TODO change this to a real dialog where we can
+ // make the input boxes turn red to indicate an error.
+ return;
+ }
+
+ terminalView.forceSize(width, height);
+ }
+ }).setNegativeButton(android.R.string.cancel, null).create().show();
+
+ return true;
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
+
+ final View view = findCurrentView(R.id.console_flip);
+ boolean activeTerminal = (view instanceof TerminalView);
+ boolean sessionOpen = false;
+ boolean disconnected = false;
+ boolean canForwardPorts = false;
+
+ if (activeTerminal) {
+ TerminalBridge bridge = ((TerminalView) view).bridge;
+ sessionOpen = bridge.isSessionOpen();
+ disconnected = bridge.isDisconnected();
+ canForwardPorts = bridge.canFowardPorts();
+ }
+
+ disconnect.setEnabled(activeTerminal);
+ if (sessionOpen || !disconnected)
+ disconnect.setTitle(R.string.list_host_disconnect);
+ else
+ disconnect.setTitle(R.string.console_menu_close);
+ copy.setEnabled(activeTerminal);
+ paste.setEnabled(clipboard.hasText() && sessionOpen);
+ portForward.setEnabled(sessionOpen && canForwardPorts);
+ urlscan.setEnabled(activeTerminal);
+ resize.setEnabled(sessionOpen);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ Intent intent = new Intent(this, HostListActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ super.onOptionsMenuClosed(menu);
+
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // connect with manager service to find all bridges
+ // when connected it will insert all views
+ bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ Log.d(TAG, "onPause called");
+
+ if (forcedOrientation && bound != null)
+ bound.setResizeAllowed(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume called");
+
+ // Make sure we don't let the screen fall asleep.
+ // This also keeps the Wi-Fi chipset from disconnecting us.
+ if (prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ configureOrientation();
+
+ if (forcedOrientation && bound != null)
+ bound.setResizeAllowed(true);
+ }
+
+ /* (non-Javadoc)
+ * @see android.app.Activity#onNewIntent(android.content.Intent)
+ */
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ Log.d(TAG, "onNewIntent called");
+
+ requested = intent.getData();
+
+ if (requested == null) {
+ Log.e(TAG, "Got null intent data in onNewIntent()");
+ return;
+ }
+
+ if (bound == null) {
+ Log.e(TAG, "We're not bound in onNewIntent()");
+ return;
+ }
+
+ TerminalBridge requestedBridge = bound.getConnectedBridge(requested.getFragment());
+ int requestedIndex = 0;
+
+ synchronized (flip) {
+ if (requestedBridge == null) {
+ // If we didn't find the requested connection, try opening it
+
+ try {
+ Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s),"+
+ "so creating one now", requested.toString(), requested.getFragment()));
+ requestedBridge = bound.openConnection(requested);
+ } catch(Exception e) {
+ Log.e(TAG, "Problem while trying to create new requested bridge from URI", e);
+ // TODO: We should display an error dialog here.
+ return;
+ }
+
+ requestedIndex = addNewTerminalView(requestedBridge);
+ } else {
+ final int flipIndex = getFlipIndex(requestedBridge);
+ if (flipIndex > requestedIndex) {
+ requestedIndex = flipIndex;
+ }
+ }
+
+ setDisplayedTerminal(requestedIndex);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ unbindService(connection);
+ }
+
+ protected void shiftCurrentTerminal(final int direction) {
+ View overlay;
+ synchronized (flip) {
+ boolean shouldAnimate = flip.getChildCount() > 1;
+
+ // Only show animation if there is something else to go to.
+ if (shouldAnimate) {
+ // keep current overlay from popping up again
+ overlay = findCurrentView(R.id.terminal_overlay);
+ if (overlay != null)
+ overlay.startAnimation(fade_stay_hidden);
+
+ if (direction == SHIFT_LEFT) {
+ flip.setInAnimation(slide_left_in);
+ flip.setOutAnimation(slide_left_out);
+ flip.showNext();
+ } else if (direction == SHIFT_RIGHT) {
+ flip.setInAnimation(slide_right_in);
+ flip.setOutAnimation(slide_right_out);
+ flip.showPrevious();
+ }
+ }
+
+ ConsoleActivity.this.updateDefault();
+
+ if (shouldAnimate) {
+ // show overlay on new slide and start fade
+ overlay = findCurrentView(R.id.terminal_overlay);
+ if (overlay != null)
+ overlay.startAnimation(fade_out_delayed);
+ }
+
+ updatePromptVisible();
+ }
+ }
+
+ /**
+ * Save the currently shown {@link TerminalView} as the default. This is
+ * saved back down into {@link TerminalManager} where we can read it again
+ * later.
+ */
+ private void updateDefault() {
+ // update the current default terminal
+ View view = findCurrentView(R.id.console_flip);
+ if(!(view instanceof TerminalView)) return;
+
+ TerminalView terminal = (TerminalView)view;
+ if(bound == null) return;
+ bound.defaultBridge = terminal.bridge;
+ }
+
+ protected void updateEmptyVisible() {
+ // update visibility of empty status message
+ empty.setVisibility((flip.getChildCount() == 0) ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Show any prompts requested by the currently visible {@link TerminalView}.
+ */
+ protected void updatePromptVisible() {
+ // check if our currently-visible terminalbridge is requesting any prompt services
+ View view = findCurrentView(R.id.console_flip);
+
+ // Hide all the prompts in case a prompt request was canceled
+ hideAllPrompts();
+
+ if(!(view instanceof TerminalView)) {
+ // we dont have an active view, so hide any prompts
+ return;
+ }
+
+ PromptHelper prompt = ((TerminalView)view).bridge.promptHelper;
+ if(String.class.equals(prompt.promptRequested)) {
+ stringPromptGroup.setVisibility(View.VISIBLE);
+
+ String instructions = prompt.promptInstructions;
+ if (instructions != null && instructions.length() > 0) {
+ stringPromptInstructions.setVisibility(View.VISIBLE);
+ stringPromptInstructions.setText(instructions);
+ } else
+ stringPromptInstructions.setVisibility(View.GONE);
+ stringPrompt.setText("");
+ stringPrompt.setHint(prompt.promptHint);
+ stringPrompt.requestFocus();
+
+ } else if(Boolean.class.equals(prompt.promptRequested)) {
+ booleanPromptGroup.setVisibility(View.VISIBLE);
+ booleanPrompt.setText(prompt.promptHint);
+ booleanYes.requestFocus();
+
+ } else {
+ hideAllPrompts();
+ view.requestFocus();
+ }
+ }
+
+ private class URLItemListener implements OnItemClickListener {
+ private WeakReference<Context> contextRef;
+
+ URLItemListener(Context context) {
+ this.contextRef = new WeakReference<Context>(context);
+ }
+
+ public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
+ Context context = contextRef.get();
+
+ if (context == null)
+ return;
+
+ try {
+ TextView urlView = (TextView) view;
+
+ String url = urlView.getText().toString();
+ if (url.indexOf("://") < 0)
+ url = "http://" + url;
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "couldn't open URL", e);
+ // We should probably tell the user that we couldn't find a handler...
+ }
+ }
+
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ Log.d(TAG, String.format("onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d", getRequestedOrientation(), newConfig.orientation));
+ if (bound != null) {
+ if (forcedOrientation &&
+ (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE &&
+ getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) ||
+ (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT &&
+ getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT))
+ bound.setResizeAllowed(false);
+ else
+ bound.setResizeAllowed(true);
+
+ bound.hardKeyboardHidden = (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
+
+ mKeyboardButton.setVisibility(bound.hardKeyboardHidden ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ /**
+ * Adds a new TerminalBridge to the current set of views in our ViewFlipper.
+ *
+ * @param bridge TerminalBridge to add to our ViewFlipper
+ * @return the child index of the new view in the ViewFlipper
+ */
+ private int addNewTerminalView(TerminalBridge bridge) {
+ // let them know about our prompt handler services
+ bridge.promptHelper.setHandler(promptHandler);
+
+ // inflate each terminal view
+ RelativeLayout view = (RelativeLayout)inflater.inflate(R.layout.item_terminal, flip, false);
+
+ // set the terminal overlay text
+ TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay);
+ overlay.setText(bridge.host.getNickname());
+
+ // and add our terminal view control, using index to place behind overlay
+ TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge);
+ terminal.setId(R.id.console_flip);
+ view.addView(terminal, 0);
+
+ synchronized (flip) {
+ // finally attach to the flipper
+ flip.addView(view);
+ return flip.getChildCount() - 1;
+ }
+ }
+
+ private int getFlipIndex(TerminalBridge bridge) {
+ synchronized (flip) {
+ final int children = flip.getChildCount();
+ for (int i = 0; i < children; i++) {
+ final View view = flip.getChildAt(i).findViewById(R.id.console_flip);
+
+ if (view == null || !(view instanceof TerminalView)) {
+ // How did that happen?
+ continue;
+ }
+
+ final TerminalView tv = (TerminalView) view;
+
+ if (tv.bridge == bridge) {
+ return i;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts.
+ *
+ * @param requestedIndex the index of the terminal view to display
+ */
+ private void setDisplayedTerminal(int requestedIndex) {
+ synchronized (flip) {
+ try {
+ // show the requested bridge if found, also fade out overlay
+ flip.setDisplayedChild(requestedIndex);
+ flip.getCurrentView().findViewById(R.id.terminal_overlay)
+ .startAnimation(fade_out_delayed);
+ } catch (NullPointerException npe) {
+ Log.d(TAG, "View went away when we were about to display it", npe);
+ }
+
+ updatePromptVisible();
+ updateEmptyVisible();
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java b/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java
new file mode 100644
index 0000000..3a438c5
--- /dev/null
+++ b/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java
@@ -0,0 +1,353 @@
+/*
+ * 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;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.util.EntropyDialog;
+import org.connectbot.util.EntropyView;
+import org.connectbot.util.OnEntropyGatheredListener;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.RadioGroup.OnCheckedChangeListener;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.trilead.ssh2.signature.ECDSASHA2Verify;
+
+public class GeneratePubkeyActivity extends Activity implements OnEntropyGatheredListener {
+ /**
+ *
+ */
+ private static final int RSA_MINIMUM_BITS = 768;
+
+ public final static String TAG = "ConnectBot.GeneratePubkeyActivity";
+
+ final static int DEFAULT_BITS = 1024;
+
+ final static int[] ECDSA_SIZES = ECDSASHA2Verify.getCurveSizes();
+
+ final static int ECDSA_DEFAULT_BITS = ECDSA_SIZES[0];
+
+ private LayoutInflater inflater = null;
+
+ private EditText nickname;
+ private RadioGroup keyTypeGroup;
+ private SeekBar bitsSlider;
+ private EditText bitsText;
+ private CheckBox unlockAtStartup;
+ private CheckBox confirmUse;
+ private Button save;
+ private Dialog entropyDialog;
+ private ProgressDialog progress;
+
+ private EditText password1, password2;
+
+ private String keyType = PubkeyDatabase.KEY_TYPE_RSA;
+ private int minBits = 768;
+ private int bits = DEFAULT_BITS;
+
+ private byte[] entropy;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.act_generatepubkey);
+
+ nickname = (EditText) findViewById(R.id.nickname);
+
+ keyTypeGroup = (RadioGroup) findViewById(R.id.key_type);
+
+ bitsText = (EditText) findViewById(R.id.bits);
+ bitsSlider = (SeekBar) findViewById(R.id.bits_slider);
+
+ password1 = (EditText) findViewById(R.id.password1);
+ password2 = (EditText) findViewById(R.id.password2);
+
+ unlockAtStartup = (CheckBox) findViewById(R.id.unlock_at_startup);
+
+ confirmUse = (CheckBox) findViewById(R.id.confirm_use);
+
+ save = (Button) findViewById(R.id.save);
+
+ inflater = LayoutInflater.from(this);
+
+ nickname.addTextChangedListener(textChecker);
+ password1.addTextChangedListener(textChecker);
+ password2.addTextChangedListener(textChecker);
+
+ keyTypeGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ if (checkedId == R.id.rsa) {
+ minBits = RSA_MINIMUM_BITS;
+
+ bitsSlider.setEnabled(true);
+ bitsSlider.setProgress(DEFAULT_BITS - minBits);
+
+ bitsText.setText(String.valueOf(DEFAULT_BITS));
+ bitsText.setEnabled(true);
+
+ keyType = PubkeyDatabase.KEY_TYPE_RSA;
+ } else if (checkedId == R.id.dsa) {
+ // DSA keys can only be 1024 bits
+
+ bitsSlider.setEnabled(false);
+ bitsSlider.setProgress(DEFAULT_BITS - minBits);
+
+ bitsText.setText(String.valueOf(DEFAULT_BITS));
+ bitsText.setEnabled(false);
+
+ keyType = PubkeyDatabase.KEY_TYPE_DSA;
+ } else if (checkedId == R.id.ec) {
+ minBits = ECDSA_DEFAULT_BITS;
+
+ bitsSlider.setEnabled(true);
+ bitsSlider.setProgress(ECDSA_DEFAULT_BITS - minBits);
+
+ bitsText.setText(String.valueOf(ECDSA_DEFAULT_BITS));
+ bitsText.setEnabled(true);
+
+ keyType = PubkeyDatabase.KEY_TYPE_EC;
+ }
+ }
+ });
+
+ bitsSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromTouch) {
+ if (PubkeyDatabase.KEY_TYPE_EC.equals(keyType)) {
+ bits = getClosestFieldSize(progress + minBits);
+ seekBar.setProgress(bits - minBits);
+ } else {
+ // Stay evenly divisible by 8 because it looks nicer to have
+ // 2048 than 2043 bits.
+ final int ourProgress = progress - (progress % 8);
+ bits = minBits + ourProgress;
+ }
+
+ bitsText.setText(String.valueOf(bits));
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // We don't care about the start.
+ }
+
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // We don't care about the stop.
+ }
+ });
+
+ bitsText.setOnFocusChangeListener(new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus) {
+ final boolean isEc = PubkeyDatabase.KEY_TYPE_EC.equals(keyType);
+ try {
+ bits = Integer.parseInt(bitsText.getText().toString());
+ if (bits < minBits) {
+ bits = minBits;
+ bitsText.setText(String.valueOf(bits));
+ }
+ if (isEc) {
+ bits = getClosestFieldSize(bits);
+ }
+ } catch (NumberFormatException nfe) {
+ bits = isEc ? ECDSA_DEFAULT_BITS : DEFAULT_BITS;
+ bitsText.setText(String.valueOf(bits));
+ }
+
+ bitsSlider.setProgress(bits - minBits);
+ }
+ }
+ });
+
+ save.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ GeneratePubkeyActivity.this.save.setEnabled(false);
+
+ GeneratePubkeyActivity.this.startEntropyGather();
+ }
+ });
+
+ }
+
+ private void checkEntries() {
+ boolean allowSave = true;
+
+ if (!password1.getText().toString().equals(password2.getText().toString()))
+ allowSave = false;
+
+ if (nickname.getText().length() == 0)
+ allowSave = false;
+
+ save.setEnabled(allowSave);
+ }
+
+ private void startEntropyGather() {
+ final View entropyView = inflater.inflate(R.layout.dia_gatherentropy, null, false);
+ ((EntropyView)entropyView.findViewById(R.id.entropy)).addOnEntropyGatheredListener(GeneratePubkeyActivity.this);
+ entropyDialog = new EntropyDialog(GeneratePubkeyActivity.this, entropyView);
+ entropyDialog.show();
+ }
+
+ public void onEntropyGathered(byte[] entropy) {
+ // For some reason the entropy dialog was aborted, exit activity
+ if (entropy == null) {
+ finish();
+ return;
+ }
+
+ this.entropy = entropy.clone();
+
+ int numSetBits = 0;
+ for (int i = 0; i < 20; i++)
+ numSetBits += measureNumberOfSetBits(this.entropy[i]);
+
+ Log.d(TAG, "Entropy distribution=" + (int)(100.0 * numSetBits / 160.0) + "%");
+
+ Log.d(TAG, "entropy gathered; attemping to generate key...");
+ startKeyGen();
+ }
+
+ private void startKeyGen() {
+ progress = new ProgressDialog(GeneratePubkeyActivity.this);
+ progress.setMessage(GeneratePubkeyActivity.this.getResources().getText(R.string.pubkey_generating));
+ progress.setIndeterminate(true);
+ progress.setCancelable(false);
+ progress.show();
+
+ Thread keyGenThread = new Thread(mKeyGen);
+ keyGenThread.setName("KeyGen");
+ keyGenThread.start();
+ }
+
+ final private Runnable mKeyGen = new Runnable() {
+ public void run() {
+ try {
+ boolean encrypted = false;
+
+ SecureRandom random = new SecureRandom();
+
+ // Work around JVM bug
+ random.nextInt();
+ random.setSeed(entropy);
+
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyType);
+
+ keyPairGen.initialize(bits, random);
+
+ KeyPair pair = keyPairGen.generateKeyPair();
+ PrivateKey priv = pair.getPrivate();
+ PublicKey pub = pair.getPublic();
+
+ String secret = password1.getText().toString();
+ if (secret.length() > 0)
+ encrypted = true;
+
+ Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv));
+ Log.d(TAG, "public: " + PubkeyUtils.formatKey(pub));
+
+ PubkeyBean pubkey = new PubkeyBean();
+ pubkey.setNickname(nickname.getText().toString());
+ pubkey.setType(keyType);
+ pubkey.setPrivateKey(PubkeyUtils.getEncodedPrivate(priv, secret));
+ pubkey.setPublicKey(pub.getEncoded());
+ pubkey.setEncrypted(encrypted);
+ pubkey.setStartup(unlockAtStartup.isChecked());
+ pubkey.setConfirmUse(confirmUse.isChecked());
+
+ PubkeyDatabase pubkeydb = new PubkeyDatabase(GeneratePubkeyActivity.this);
+ pubkeydb.savePubkey(pubkey);
+ pubkeydb.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Could not generate key pair");
+
+ e.printStackTrace();
+ }
+
+ GeneratePubkeyActivity.this.runOnUiThread(new Runnable() {
+ public void run() {
+ progress.dismiss();
+ GeneratePubkeyActivity.this.finish();
+ }
+ });
+ }
+
+ };
+
+ final private TextWatcher textChecker = new TextWatcher() {
+ public void afterTextChanged(Editable s) {}
+
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {}
+
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ checkEntries();
+ }
+ };
+
+ private int measureNumberOfSetBits(byte b) {
+ int numSetBits = 0;
+
+ for (int i = 0; i < 8; i++) {
+ if ((b & 1) == 1)
+ numSetBits++;
+ b >>= 1;
+ }
+
+ return numSetBits;
+ }
+
+ private int getClosestFieldSize(int bits) {
+ int outBits = ECDSA_DEFAULT_BITS;
+ int distance = Math.abs(bits - ECDSA_DEFAULT_BITS);
+
+ for (int i = 1; i < ECDSA_SIZES.length; i++) {
+ int thisDistance = Math.abs(bits - ECDSA_SIZES[i]);
+ if (thisDistance < distance) {
+ distance = thisDistance;
+ outBits = ECDSA_SIZES[i];
+ }
+ }
+ return outBits;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/HelpActivity.java b/app/src/main/java/org/connectbot/HelpActivity.java
new file mode 100644
index 0000000..d82777d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/HelpActivity.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpActivity extends Activity {
+ public final static String TAG = "ConnectBot.HelpActivity";
+
+ public final static String HELPDIR = "help";
+ public final static String SUFFIX = ".html";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.act_help);
+
+ this.setTitle(String.format("%s: %s",
+ getResources().getText(R.string.app_name),
+ getResources().getText(R.string.title_help)));
+
+ AssetManager am = this.getAssets();
+ LinearLayout content = (LinearLayout)this.findViewById(R.id.topics);
+
+ try {
+ for (String name : am.list(HELPDIR)) {
+ if (name.endsWith(SUFFIX)) {
+ Button button = new Button(this);
+ final String topic = name.substring(0, name.length() - SUFFIX.length());
+ button.setText(topic);
+
+ button.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(HelpActivity.this, HelpTopicActivity.class);
+ intent.putExtra(Intent.EXTRA_TITLE, topic);
+ HelpActivity.this.startActivity(intent);
+ }
+ });
+
+ content.addView(button);
+ }
+ }
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ Log.e(TAG, "couldn't get list of help assets", e);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/HelpTopicActivity.java b/app/src/main/java/org/connectbot/HelpTopicActivity.java
new file mode 100644
index 0000000..a5fa8e0
--- /dev/null
+++ b/app/src/main/java/org/connectbot/HelpTopicActivity.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import org.connectbot.util.HelpTopicView;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpTopicActivity extends Activity {
+ public final static String TAG = "ConnectBot.HelpActivity";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.act_help_topic);
+
+ String topic = getIntent().getStringExtra(Intent.EXTRA_TITLE);
+
+ this.setTitle(String.format("%s: %s - %s",
+ getResources().getText(R.string.app_name),
+ getResources().getText(R.string.title_help),
+ topic));
+
+ HelpTopicView helpTopic = (HelpTopicView) findViewById(R.id.topic_text);
+
+ helpTopic.setTopic(topic);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/HostEditorActivity.java b/app/src/main/java/org/connectbot/HostEditorActivity.java
new file mode 100644
index 0000000..4e8427f
--- /dev/null
+++ b/app/src/main/java/org/connectbot/HostEditorActivity.java
@@ -0,0 +1,433 @@
+/*
+ * 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;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PubkeyDatabase;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+
+public class HostEditorActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {
+ public class CursorPreferenceHack implements SharedPreferences {
+ protected final String table;
+ protected final long id;
+
+ protected Map<String, String> values = new HashMap<String, String>();
+// protected Map<String, String> pubkeys = new HashMap<String, String>();
+
+ public CursorPreferenceHack(String table, long id) {
+ this.table = table;
+ this.id = id;
+
+ cacheValues();
+ }
+
+ protected final void cacheValues() {
+ // fill a cursor and cache the values locally
+ // this makes sure we dont have any floating cursor to dispose later
+
+ SQLiteDatabase db = hostdb.getReadableDatabase();
+ Cursor cursor = db.query(table, null, "_id = ?",
+ new String[] { String.valueOf(id) }, null, null, null);
+
+ if (cursor.moveToFirst()) {
+ for(int i = 0; i < cursor.getColumnCount(); i++) {
+ String key = cursor.getColumnName(i);
+ if(key.equals(HostDatabase.FIELD_HOST_HOSTKEY)) continue;
+ String value = cursor.getString(i);
+ values.put(key, value);
+ }
+ }
+ cursor.close();
+ db.close();
+
+// db = pubkeydb.getReadableDatabase();
+// cursor = db.query(PubkeyDatabase.TABLE_PUBKEYS,
+// new String[] { "_id", PubkeyDatabase.FIELD_PUBKEY_NICKNAME },
+// null, null, null, null, null);
+//
+// if (cursor.moveToFirst()) {
+// do {
+// String pubkeyid = String.valueOf(cursor.getLong(0));
+// String value = cursor.getString(1);
+// pubkeys.put(pubkeyid, value);
+// } while (cursor.moveToNext());
+// }
+//
+// cursor.close();
+// db.close();
+ }
+
+ public boolean contains(String key) {
+ return values.containsKey(key);
+ }
+
+ public class Editor implements SharedPreferences.Editor {
+
+ private ContentValues update = new ContentValues();
+
+ public SharedPreferences.Editor clear() {
+ Log.d(this.getClass().toString(), "clear()");
+ update = new ContentValues();
+ return this;
+ }
+
+ public boolean commit() {
+ //Log.d(this.getClass().toString(), "commit() changes back to database");
+ SQLiteDatabase db = hostdb.getWritableDatabase();
+ db.update(table, update, "_id = ?", new String[] { String.valueOf(id) });
+ db.close();
+
+ // make sure we refresh the parent cached values
+ cacheValues();
+
+ // and update any listeners
+ for(OnSharedPreferenceChangeListener listener : listeners) {
+ listener.onSharedPreferenceChanged(CursorPreferenceHack.this, null);
+ }
+
+ return true;
+ }
+
+ // Gingerbread compatibility
+ public void apply() {
+ commit();
+ }
+
+ public android.content.SharedPreferences.Editor putBoolean(String key, boolean value) {
+ return this.putString(key, Boolean.toString(value));
+ }
+
+ public android.content.SharedPreferences.Editor putFloat(String key, float value) {
+ return this.putString(key, Float.toString(value));
+ }
+
+ public android.content.SharedPreferences.Editor putInt(String key, int value) {
+ return this.putString(key, Integer.toString(value));
+ }
+
+ public android.content.SharedPreferences.Editor putLong(String key, long value) {
+ return this.putString(key, Long.toString(value));
+ }
+
+ public android.content.SharedPreferences.Editor putString(String key, String value) {
+ //Log.d(this.getClass().toString(), String.format("Editor.putString(key=%s, value=%s)", key, value));
+ update.put(key, value);
+ return this;
+ }
+
+ public android.content.SharedPreferences.Editor remove(String key) {
+ //Log.d(this.getClass().toString(), String.format("Editor.remove(key=%s)", key));
+ update.remove(key);
+ return this;
+ }
+
+ public android.content.SharedPreferences.Editor putStringSet(String key, Set<String> value) {
+ throw new UnsupportedOperationException("HostEditor Prefs do not support Set<String>");
+ }
+ }
+
+
+ public Editor edit() {
+ //Log.d(this.getClass().toString(), "edit()");
+ return new Editor();
+ }
+
+ public Map<String, ?> getAll() {
+ return values;
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ return Boolean.valueOf(this.getString(key, Boolean.toString(defValue)));
+ }
+
+ public float getFloat(String key, float defValue) {
+ return Float.valueOf(this.getString(key, Float.toString(defValue)));
+ }
+
+ public int getInt(String key, int defValue) {
+ return Integer.valueOf(this.getString(key, Integer.toString(defValue)));
+ }
+
+ public long getLong(String key, long defValue) {
+ return Long.valueOf(this.getString(key, Long.toString(defValue)));
+ }
+
+ public String getString(String key, String defValue) {
+ //Log.d(this.getClass().toString(), String.format("getString(key=%s, defValue=%s)", key, defValue));
+
+ if(!values.containsKey(key)) return defValue;
+ return values.get(key);
+ }
+
+ public Set<String> getStringSet(String key, Set<String> defValue) {
+ throw new ClassCastException("HostEditor Prefs do not support Set<String>");
+ }
+
+ protected List<OnSharedPreferenceChangeListener> listeners = new LinkedList<OnSharedPreferenceChangeListener>();
+
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ //Log.d(this.getClass().toString(), String.format("getSharedPreferences(name=%s)", name));
+ return this.pref;
+ }
+
+ protected static final String TAG = "ConnectBot.HostEditorActivity";
+
+ protected HostDatabase hostdb = null;
+ private PubkeyDatabase pubkeydb = null;
+
+ private CursorPreferenceHack pref;
+ private ServiceConnection connection;
+
+ private HostBean host;
+ protected TerminalBridge hostBridge;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ long hostId = this.getIntent().getLongExtra(Intent.EXTRA_TITLE, -1);
+
+ // TODO: we could pass through a specific ContentProvider uri here
+ //this.getPreferenceManager().setSharedPreferencesName(uri);
+
+ this.hostdb = new HostDatabase(this);
+ this.pubkeydb = new PubkeyDatabase(this);
+
+ host = hostdb.findHostById(hostId);
+
+ connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ TerminalManager bound = ((TerminalManager.TerminalBinder) service).getService();
+
+ hostBridge = bound.getConnectedBridge(host);
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ hostBridge = null;
+ }
+ };
+
+ this.pref = new CursorPreferenceHack(HostDatabase.TABLE_HOSTS, hostId);
+ this.pref.registerOnSharedPreferenceChangeListener(this);
+
+ this.addPreferencesFromResource(R.xml.host_prefs);
+
+ // add all existing pubkeys to our listpreference for user to choose from
+ // TODO: may be an issue here when this activity is recycled after adding a new pubkey
+ // TODO: should consider moving into onStart, but we dont have a good way of resetting the listpref after filling once
+ ListPreference pubkeyPref = (ListPreference)this.findPreference(HostDatabase.FIELD_HOST_PUBKEYID);
+
+ List<CharSequence> pubkeyNicks = new LinkedList<CharSequence>(Arrays.asList(pubkeyPref.getEntries()));
+ pubkeyNicks.addAll(pubkeydb.allValues(PubkeyDatabase.FIELD_PUBKEY_NICKNAME));
+ pubkeyPref.setEntries(pubkeyNicks.toArray(new CharSequence[pubkeyNicks.size()]));
+
+ List<CharSequence> pubkeyIds = new LinkedList<CharSequence>(Arrays.asList(pubkeyPref.getEntryValues()));
+ pubkeyIds.addAll(pubkeydb.allValues("_id"));
+ pubkeyPref.setEntryValues(pubkeyIds.toArray(new CharSequence[pubkeyIds.size()]));
+
+ // Populate the character set encoding list with all available
+ final ListPreference charsetPref = (ListPreference) findPreference(HostDatabase.FIELD_HOST_ENCODING);
+
+ if (CharsetHolder.isInitialized()) {
+ initCharsetPref(charsetPref);
+ } else {
+ String[] currentCharsetPref = new String[1];
+ currentCharsetPref[0] = charsetPref.getValue();
+ charsetPref.setEntryValues(currentCharsetPref);
+ charsetPref.setEntries(currentCharsetPref);
+
+ new Thread(new Runnable() {
+ public void run() {
+ initCharsetPref(charsetPref);
+ }
+ }).start();
+ }
+
+ this.updateSummaries();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ if(this.hostdb == null)
+ this.hostdb = new HostDatabase(this);
+
+ if(this.pubkeydb == null)
+ this.pubkeydb = new PubkeyDatabase(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ unbindService(connection);
+
+ if(this.hostdb != null) {
+ this.hostdb.close();
+ this.hostdb = null;
+ }
+
+ if(this.pubkeydb != null) {
+ this.pubkeydb.close();
+ this.pubkeydb = null;
+ }
+ }
+
+ private void updateSummaries() {
+ // for all text preferences, set hint as current database value
+ for (String key : this.pref.values.keySet()) {
+ if(key.equals(HostDatabase.FIELD_HOST_POSTLOGIN)) continue;
+ Preference pref = this.findPreference(key);
+ if(pref == null) continue;
+ if(pref instanceof CheckBoxPreference) continue;
+ CharSequence value = this.pref.getString(key, "");
+
+ if (key.equals(HostDatabase.FIELD_HOST_PUBKEYID)) {
+ try {
+ int pubkeyId = Integer.parseInt((String) value);
+ if (pubkeyId >= 0)
+ pref.setSummary(pubkeydb.getNickname(pubkeyId));
+ else if(pubkeyId == HostDatabase.PUBKEYID_ANY)
+ pref.setSummary(R.string.list_pubkeyids_any);
+ else if(pubkeyId == HostDatabase.PUBKEYID_NEVER)
+ pref.setSummary(R.string.list_pubkeyids_none);
+ continue;
+ } catch (NumberFormatException nfe) {
+ // Fall through.
+ }
+ } else if (pref instanceof ListPreference) {
+ ListPreference listPref = (ListPreference) pref;
+ int entryIndex = listPref.findIndexOfValue((String) value);
+ if (entryIndex >= 0)
+ value = listPref.getEntries()[entryIndex];
+ }
+
+ pref.setSummary(value);
+ }
+
+ }
+
+ private void initCharsetPref(final ListPreference charsetPref) {
+ charsetPref.setEntryValues(CharsetHolder.getCharsetIds());
+ charsetPref.setEntries(CharsetHolder.getCharsetNames());
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ // update values on changed preference
+ this.updateSummaries();
+
+ // Our CursorPreferenceHack always send null keys, so try to set charset anyway
+ if (hostBridge != null)
+ hostBridge.setCharset(sharedPreferences
+ .getString(HostDatabase.FIELD_HOST_ENCODING, HostDatabase.ENCODING_DEFAULT));
+ }
+
+ public static class CharsetHolder {
+ private static boolean initialized = false;
+
+ private static CharSequence[] charsetIds;
+ private static CharSequence[] charsetNames;
+
+ public static CharSequence[] getCharsetNames() {
+ if (charsetNames == null)
+ initialize();
+
+ return charsetNames;
+ }
+
+ public static CharSequence[] getCharsetIds() {
+ if (charsetIds == null)
+ initialize();
+
+ return charsetIds;
+ }
+
+ private synchronized static void initialize() {
+ if (initialized)
+ return;
+
+ List<CharSequence> charsetIdsList = new LinkedList<CharSequence>();
+ List<CharSequence> charsetNamesList = new LinkedList<CharSequence>();
+
+ for (Entry<String, Charset> entry : Charset.availableCharsets().entrySet()) {
+ Charset c = entry.getValue();
+ if (c.canEncode() && c.isRegistered()) {
+ String key = entry.getKey();
+ if (key.startsWith("cp")) {
+ // Custom CP437 charset changes
+ charsetIdsList.add("CP437");
+ charsetNamesList.add("CP437");
+ }
+ charsetIdsList.add(entry.getKey());
+ charsetNamesList.add(c.displayName());
+ }
+ }
+
+ charsetIds = charsetIdsList.toArray(new CharSequence[charsetIdsList.size()]);
+ charsetNames = charsetNamesList.toArray(new CharSequence[charsetNamesList.size()]);
+
+ initialized = true;
+ }
+
+ public static boolean isInitialized() {
+ return initialized;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/HostListActivity.java b/app/src/main/java/org/connectbot/HostListActivity.java
new file mode 100644
index 0000000..648b705
--- /dev/null
+++ b/app/src/main/java/org/connectbot/HostListActivity.java
@@ -0,0 +1,570 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.transport.TransportFactory;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PreferenceConstants;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.Intent.ShortcutIconResource;
+import android.content.SharedPreferences.Editor;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View.OnKeyListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+public class HostListActivity extends ListActivity {
+ public final static int REQUEST_EDIT = 1;
+
+ public final static int REQUEST_EULA = 2;
+
+ protected TerminalManager bound = null;
+
+ protected HostDatabase hostdb;
+ private List<HostBean> hosts;
+ protected LayoutInflater inflater = null;
+
+ protected boolean sortedByColor = false;
+
+ private MenuItem sortcolor;
+
+ private MenuItem sortlast;
+
+ private Spinner transportSpinner;
+ private TextView quickconnect;
+
+ private SharedPreferences prefs = null;
+
+ protected boolean makingShortcut = false;
+
+ protected Handler updateHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ HostListActivity.this.updateList();
+ }
+ };
+
+ private ServiceConnection connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ bound = ((TerminalManager.TerminalBinder) service).getService();
+
+ // update our listview binder to find the service
+ HostListActivity.this.updateList();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ bound = null;
+ HostListActivity.this.updateList();
+ }
+ };
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // start the terminal manager service
+ this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ if(this.hostdb == null)
+ this.hostdb = new HostDatabase(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ this.unbindService(connection);
+
+ if(this.hostdb != null) {
+ this.hostdb.close();
+ this.hostdb = null;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_EULA) {
+ if(resultCode == Activity.RESULT_OK) {
+ // yay they agreed, so store that info
+ Editor edit = prefs.edit();
+ edit.putBoolean(PreferenceConstants.EULA, true);
+ edit.commit();
+ } else {
+ // user didnt agree, so close
+ this.finish();
+ }
+ } else if (requestCode == REQUEST_EDIT) {
+ this.updateList();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.act_hostlist);
+
+ this.setTitle(String.format("%s: %s",
+ getResources().getText(R.string.app_name),
+ getResources().getText(R.string.title_hosts_list)));
+
+ this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // detect HTC Dream and apply special preferences
+ if (Build.MANUFACTURER.equals("HTC") && Build.DEVICE.equals("dream")) {
+ if (!prefs.contains(PreferenceConstants.SHIFT_FKEYS) &&
+ !prefs.contains(PreferenceConstants.CTRL_FKEYS)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PreferenceConstants.SHIFT_FKEYS, true);
+ editor.putBoolean(PreferenceConstants.CTRL_FKEYS, true);
+ editor.commit();
+ }
+ }
+
+ // check for eula agreement
+ boolean agreed = prefs.getBoolean(PreferenceConstants.EULA, false);
+ if(!agreed) {
+ this.startActivityForResult(new Intent(this, WizardActivity.class), REQUEST_EULA);
+ }
+
+ this.makingShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())
+ || Intent.ACTION_PICK.equals(getIntent().getAction());
+
+ // connect with hosts database and populate list
+ this.hostdb = new HostDatabase(this);
+ ListView list = this.getListView();
+
+ this.sortedByColor = prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false);
+
+ //this.list.setSelector(R.drawable.highlight_disabled_pressed);
+
+ list.setOnItemClickListener(new OnItemClickListener() {
+
+ public synchronized void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+
+ // launch off to console details
+ HostBean host = (HostBean) parent.getAdapter().getItem(position);
+ Uri uri = host.getUri();
+
+ Intent contents = new Intent(Intent.ACTION_VIEW, uri);
+ contents.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ if (makingShortcut) {
+ // create shortcut if requested
+ ShortcutIconResource icon = Intent.ShortcutIconResource.fromContext(HostListActivity.this, R.drawable.icon);
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, contents);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, host.getNickname());
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
+
+ setResult(RESULT_OK, intent);
+ finish();
+
+ } else {
+ // otherwise just launch activity to show this host
+ HostListActivity.this.startActivity(contents);
+ }
+ }
+ });
+
+ this.registerForContextMenu(list);
+
+ quickconnect = (TextView) this.findViewById(R.id.front_quickconnect);
+ quickconnect.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
+ quickconnect.setOnKeyListener(new OnKeyListener() {
+
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+
+ if(event.getAction() == KeyEvent.ACTION_UP) return false;
+ if(keyCode != KeyEvent.KEYCODE_ENTER) return false;
+
+ return startConsoleActivity();
+ }
+ });
+
+ transportSpinner = (Spinner)findViewById(R.id.transport_selection);
+ transportSpinner.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
+ ArrayAdapter<String> transportSelection = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, TransportFactory.getTransportNames());
+ transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ transportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> arg0, View view, int position, long id) {
+ String formatHint = TransportFactory.getFormatHint(
+ (String) transportSpinner.getSelectedItem(),
+ HostListActivity.this);
+
+ quickconnect.setHint(formatHint);
+ quickconnect.setError(null);
+ quickconnect.requestFocus();
+ }
+ public void onNothingSelected(AdapterView<?> arg0) { }
+ });
+ transportSpinner.setAdapter(transportSelection);
+
+ this.inflater = LayoutInflater.from(this);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ // don't offer menus when creating shortcut
+ if (makingShortcut) return true;
+
+ sortcolor.setVisible(!sortedByColor);
+ sortlast.setVisible(sortedByColor);
+
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ // don't offer menus when creating shortcut
+ if(makingShortcut) return true;
+
+ // add host, ssh keys, about
+ sortcolor = menu.add(R.string.list_menu_sortcolor);
+ sortcolor.setIcon(android.R.drawable.ic_menu_share);
+ sortcolor.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ sortedByColor = true;
+ updateList();
+ return true;
+ }
+ });
+
+ sortlast = menu.add(R.string.list_menu_sortname);
+ sortlast.setIcon(android.R.drawable.ic_menu_share);
+ sortlast.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ sortedByColor = false;
+ updateList();
+ return true;
+ }
+ });
+
+ MenuItem keys = menu.add(R.string.list_menu_pubkeys);
+ keys.setIcon(android.R.drawable.ic_lock_lock);
+ keys.setIntent(new Intent(HostListActivity.this, PubkeyListActivity.class));
+
+ MenuItem colors = menu.add(R.string.title_colors);
+ colors.setIcon(android.R.drawable.ic_menu_slideshow);
+ colors.setIntent(new Intent(HostListActivity.this, ColorsActivity.class));
+
+ MenuItem settings = menu.add(R.string.list_menu_settings);
+ settings.setIcon(android.R.drawable.ic_menu_preferences);
+ settings.setIntent(new Intent(HostListActivity.this, SettingsActivity.class));
+
+ MenuItem help = menu.add(R.string.title_help);
+ help.setIcon(android.R.drawable.ic_menu_help);
+ help.setIntent(new Intent(HostListActivity.this, HelpActivity.class));
+
+ return true;
+
+ }
+
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+
+ // create menu to handle hosts
+
+ // create menu to handle deleting and sharing lists
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ final HostBean host = (HostBean) this.getListView().getItemAtPosition(info.position);
+
+ menu.setHeaderTitle(host.getNickname());
+
+ // edit, disconnect, delete
+ MenuItem connect = menu.add(R.string.list_host_disconnect);
+ final TerminalBridge bridge = bound.getConnectedBridge(host);
+ connect.setEnabled((bridge != null));
+ connect.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ bridge.dispatchDisconnect(true);
+ updateHandler.sendEmptyMessage(-1);
+ return true;
+ }
+ });
+
+ MenuItem edit = menu.add(R.string.list_host_edit);
+ edit.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent(HostListActivity.this, HostEditorActivity.class);
+ intent.putExtra(Intent.EXTRA_TITLE, host.getId());
+ HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
+ return true;
+ }
+ });
+
+ MenuItem portForwards = menu.add(R.string.list_host_portforwards);
+ portForwards.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent(HostListActivity.this, PortForwardListActivity.class);
+ intent.putExtra(Intent.EXTRA_TITLE, host.getId());
+ HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
+ return true;
+ }
+ });
+ if (!TransportFactory.canForwardPorts(host.getProtocol()))
+ portForwards.setEnabled(false);
+
+ MenuItem delete = menu.add(R.string.list_host_delete);
+ delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // prompt user to make sure they really want this
+ new AlertDialog.Builder(HostListActivity.this)
+ .setMessage(getString(R.string.delete_message, host.getNickname()))
+ .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // make sure we disconnect
+ if(bridge != null)
+ bridge.dispatchDisconnect(true);
+
+ hostdb.deleteHost(host);
+ updateHandler.sendEmptyMessage(-1);
+ }
+ })
+ .setNegativeButton(R.string.delete_neg, null).create().show();
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * @param text
+ * @return
+ */
+ private boolean startConsoleActivity() {
+ Uri uri = TransportFactory.getUri((String) transportSpinner
+ .getSelectedItem(), quickconnect.getText().toString());
+
+ if (uri == null) {
+ quickconnect.setError(getString(R.string.list_format_error,
+ TransportFactory.getFormatHint(
+ (String) transportSpinner.getSelectedItem(),
+ HostListActivity.this)));
+ return false;
+ }
+
+ HostBean host = TransportFactory.findHost(hostdb, uri);
+ if (host == null) {
+ host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
+ host.setColor(HostDatabase.COLOR_GRAY);
+ host.setPubkeyId(HostDatabase.PUBKEYID_ANY);
+ hostdb.saveHost(host);
+ }
+
+ Intent intent = new Intent(HostListActivity.this, ConsoleActivity.class);
+ intent.setData(uri);
+ startActivity(intent);
+
+ return true;
+ }
+
+ protected void updateList() {
+ if (prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false) != sortedByColor) {
+ Editor edit = prefs.edit();
+ edit.putBoolean(PreferenceConstants.SORT_BY_COLOR, sortedByColor);
+ edit.commit();
+ }
+
+ if (hostdb == null)
+ hostdb = new HostDatabase(this);
+
+ hosts = hostdb.getHosts(sortedByColor);
+
+ // Don't lose hosts that are connected via shortcuts but not in the database.
+ if (bound != null) {
+ for (TerminalBridge bridge : bound.bridges) {
+ if (!hosts.contains(bridge.host))
+ hosts.add(0, bridge.host);
+ }
+ }
+
+ HostAdapter adapter = new HostAdapter(this, hosts, bound);
+
+ this.setListAdapter(adapter);
+ }
+
+ class HostAdapter extends ArrayAdapter<HostBean> {
+ private List<HostBean> hosts;
+ private final TerminalManager manager;
+ private final ColorStateList red, green, blue;
+
+ public final static int STATE_UNKNOWN = 1, STATE_CONNECTED = 2, STATE_DISCONNECTED = 3;
+
+ class ViewHolder {
+ public TextView nickname;
+ public TextView caption;
+ public ImageView icon;
+ }
+
+ public HostAdapter(Context context, List<HostBean> hosts, TerminalManager manager) {
+ super(context, R.layout.item_host, hosts);
+
+ this.hosts = hosts;
+ this.manager = manager;
+
+ red = context.getResources().getColorStateList(R.color.red);
+ green = context.getResources().getColorStateList(R.color.green);
+ blue = context.getResources().getColorStateList(R.color.blue);
+ }
+
+ /**
+ * Check if we're connected to a terminal with the given host.
+ */
+ private int getConnectedState(HostBean host) {
+ // always disconnected if we dont have backend service
+ if (this.manager == null)
+ return STATE_UNKNOWN;
+
+ if (manager.getConnectedBridge(host) != null)
+ return STATE_CONNECTED;
+
+ if (manager.disconnected.contains(host))
+ return STATE_DISCONNECTED;
+
+ return STATE_UNKNOWN;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.item_host, null, false);
+
+ holder = new ViewHolder();
+
+ holder.nickname = (TextView)convertView.findViewById(android.R.id.text1);
+ holder.caption = (TextView)convertView.findViewById(android.R.id.text2);
+ holder.icon = (ImageView)convertView.findViewById(android.R.id.icon);
+
+ convertView.setTag(holder);
+ } else
+ holder = (ViewHolder) convertView.getTag();
+
+ HostBean host = hosts.get(position);
+ if (host == null) {
+ // Well, something bad happened. We can't continue.
+ Log.e("HostAdapter", "Host bean is null!");
+
+ holder.nickname.setText("Error during lookup");
+ holder.caption.setText("see 'adb logcat' for more");
+ return convertView;
+ }
+
+ holder.nickname.setText(host.getNickname());
+
+ switch (this.getConnectedState(host)) {
+ case STATE_UNKNOWN:
+ holder.icon.setImageState(new int[] { }, true);
+ break;
+ case STATE_CONNECTED:
+ holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true);
+ break;
+ case STATE_DISCONNECTED:
+ holder.icon.setImageState(new int[] { android.R.attr.state_expanded }, true);
+ break;
+ }
+
+ ColorStateList chosen = null;
+ if (HostDatabase.COLOR_RED.equals(host.getColor()))
+ chosen = this.red;
+ else if (HostDatabase.COLOR_GREEN.equals(host.getColor()))
+ chosen = this.green;
+ else if (HostDatabase.COLOR_BLUE.equals(host.getColor()))
+ chosen = this.blue;
+
+ Context context = convertView.getContext();
+
+ if (chosen != null) {
+ // set color normally if not selected
+ holder.nickname.setTextColor(chosen);
+ holder.caption.setTextColor(chosen);
+ } else {
+ // selected, so revert back to default black text
+ holder.nickname.setTextAppearance(context, android.R.attr.textAppearanceLarge);
+ holder.caption.setTextAppearance(context, android.R.attr.textAppearanceSmall);
+ }
+
+ long now = System.currentTimeMillis() / 1000;
+
+ String nice = context.getString(R.string.bind_never);
+ if (host.getLastConnect() > 0) {
+ int minutes = (int)((now - host.getLastConnect()) / 60);
+ if (minutes >= 60) {
+ int hours = (minutes / 60);
+ if (hours >= 24) {
+ int days = (hours / 24);
+ nice = context.getString(R.string.bind_days, days);
+ } else
+ nice = context.getString(R.string.bind_hours, hours);
+ } else
+ nice = context.getString(R.string.bind_minutes, minutes);
+ }
+
+ holder.caption.setText(nice);
+
+ return convertView;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/PortForwardListActivity.java b/app/src/main/java/org/connectbot/PortForwardListActivity.java
new file mode 100644
index 0000000..f9982e4
--- /dev/null
+++ b/app/src/main/java/org/connectbot/PortForwardListActivity.java
@@ -0,0 +1,425 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.database.SQLException;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+/**
+ * List all portForwards for a particular host and provide a way for users to add more portForwards,
+ * edit existing portForwards, and delete portForwards.
+ *
+ * @author Kenny Root
+ */
+public class PortForwardListActivity extends ListActivity {
+ public final static String TAG = "ConnectBot.PortForwardListActivity";
+
+ private static final int LISTENER_CYCLE_TIME = 500;
+
+ protected HostDatabase hostdb;
+
+ private List<PortForwardBean> portForwards;
+
+ private ServiceConnection connection = null;
+ protected TerminalBridge hostBridge = null;
+ protected LayoutInflater inflater = null;
+
+ private HostBean host;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ if(this.hostdb == null)
+ this.hostdb = new HostDatabase(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ this.unbindService(connection);
+
+ if(this.hostdb != null) {
+ this.hostdb.close();
+ this.hostdb = null;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ long hostId = this.getIntent().getLongExtra(Intent.EXTRA_TITLE, -1);
+
+ setContentView(R.layout.act_portforwardlist);
+
+ // connect with hosts database and populate list
+ this.hostdb = new HostDatabase(this);
+ host = hostdb.findHostById(hostId);
+
+ {
+ final String nickname = host != null ? host.getNickname() : null;
+ final Resources resources = getResources();
+
+ if (nickname != null) {
+ this.setTitle(String.format("%s: %s (%s)",
+ resources.getText(R.string.app_name),
+ resources.getText(R.string.title_port_forwards_list),
+ nickname));
+ } else {
+ this.setTitle(String.format("%s: %s",
+ resources.getText(R.string.app_name),
+ resources.getText(R.string.title_port_forwards_list)));
+ }
+ }
+
+ connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ TerminalManager bound = ((TerminalManager.TerminalBinder) service).getService();
+
+ hostBridge = bound.getConnectedBridge(host);
+ updateHandler.sendEmptyMessage(-1);
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ hostBridge = null;
+ }
+ };
+
+ this.updateList();
+
+ this.registerForContextMenu(this.getListView());
+
+ this.getListView().setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
+ ListView lv = PortForwardListActivity.this.getListView();
+ PortForwardBean pfb = (PortForwardBean) lv.getItemAtPosition(position);
+
+ if (hostBridge != null) {
+ if (pfb.isEnabled())
+ hostBridge.disablePortForward(pfb);
+ else {
+ if (!hostBridge.enablePortForward(pfb))
+ Toast.makeText(PortForwardListActivity.this, getString(R.string.portforward_problem), Toast.LENGTH_LONG).show();
+ }
+
+ updateHandler.sendEmptyMessage(-1);
+ }
+ }
+ });
+
+ this.inflater = LayoutInflater.from(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem add = menu.add(R.string.portforward_menu_add);
+ add.setIcon(android.R.drawable.ic_menu_add);
+ add.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // build dialog to prompt user about updating
+ final View portForwardView = inflater.inflate(R.layout.dia_portforward, null, false);
+ final EditText destEdit = (EditText) portForwardView.findViewById(R.id.portforward_destination);
+ final Spinner typeSpinner = (Spinner)portForwardView.findViewById(R.id.portforward_type);
+
+ typeSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> value, View view,
+ int position, long id) {
+ destEdit.setEnabled(position != 2);
+ }
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+ });
+
+ new AlertDialog.Builder(PortForwardListActivity.this)
+ .setView(portForwardView)
+ .setPositiveButton(R.string.portforward_pos, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ final EditText nicknameEdit = (EditText) portForwardView.findViewById(R.id.nickname);
+ final EditText sourcePortEdit = (EditText) portForwardView.findViewById(R.id.portforward_source);
+
+ String type = HostDatabase.PORTFORWARD_LOCAL;
+ switch (typeSpinner.getSelectedItemPosition()) {
+ case 0:
+ type = HostDatabase.PORTFORWARD_LOCAL;
+ break;
+ case 1:
+ type = HostDatabase.PORTFORWARD_REMOTE;
+ break;
+ case 2:
+ type = HostDatabase.PORTFORWARD_DYNAMIC5;
+ break;
+ }
+
+ PortForwardBean pfb = new PortForwardBean(
+ host != null ? host.getId() : -1,
+ nicknameEdit.getText().toString(), type,
+ sourcePortEdit.getText().toString(),
+ destEdit.getText().toString());
+
+ if (hostBridge != null) {
+ hostBridge.addPortForward(pfb);
+ hostBridge.enablePortForward(pfb);
+ }
+
+ if (host != null && !hostdb.savePortForward(pfb))
+ throw new SQLException("Could not save port forward");
+
+ updateHandler.sendEmptyMessage(-1);
+ } catch (Exception e) {
+ Log.e(TAG, "Could not update port forward", e);
+ // TODO Show failure dialog.
+ }
+ }
+ })
+ .setNegativeButton(R.string.delete_neg, null).create().show();
+
+ return true;
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ // Create menu to handle deleting and editing port forward
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ final PortForwardBean pfb = (PortForwardBean) this.getListView().getItemAtPosition(info.position);
+
+ menu.setHeaderTitle(pfb.getNickname());
+
+ MenuItem edit = menu.add(R.string.portforward_edit);
+ edit.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ final View editTunnelView = inflater.inflate(R.layout.dia_portforward, null, false);
+
+ final Spinner typeSpinner = (Spinner) editTunnelView.findViewById(R.id.portforward_type);
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(pfb.getType()))
+ typeSpinner.setSelection(0);
+ else if (HostDatabase.PORTFORWARD_REMOTE.equals(pfb.getType()))
+ typeSpinner.setSelection(1);
+ else
+ typeSpinner.setSelection(2);
+
+ final EditText nicknameEdit = (EditText) editTunnelView.findViewById(R.id.nickname);
+ nicknameEdit.setText(pfb.getNickname());
+
+ final EditText sourcePortEdit = (EditText) editTunnelView.findViewById(R.id.portforward_source);
+ sourcePortEdit.setText(String.valueOf(pfb.getSourcePort()));
+
+ final EditText destEdit = (EditText) editTunnelView.findViewById(R.id.portforward_destination);
+ if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(pfb.getType())) {
+ destEdit.setEnabled(false);
+ } else {
+ destEdit.setText(String.format("%s:%d", pfb.getDestAddr(), pfb.getDestPort()));
+ }
+
+ typeSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> value, View view,
+ int position, long id) {
+ destEdit.setEnabled(position != 2);
+ }
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+ });
+
+ new AlertDialog.Builder(PortForwardListActivity.this)
+ .setView(editTunnelView)
+ .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ if (hostBridge != null)
+ hostBridge.disablePortForward(pfb);
+
+ pfb.setNickname(nicknameEdit.getText().toString());
+
+ switch (typeSpinner.getSelectedItemPosition()) {
+ case 0:
+ pfb.setType(HostDatabase.PORTFORWARD_LOCAL);
+ break;
+ case 1:
+ pfb.setType(HostDatabase.PORTFORWARD_REMOTE);
+ break;
+ case 2:
+ pfb.setType(HostDatabase.PORTFORWARD_DYNAMIC5);
+ break;
+ }
+
+ pfb.setSourcePort(Integer.parseInt(sourcePortEdit.getText().toString()));
+ pfb.setDest(destEdit.getText().toString());
+
+ // Use the new settings for the existing connection.
+ if (hostBridge != null)
+ updateHandler.postDelayed(new Runnable() {
+ public void run() {
+ hostBridge.enablePortForward(pfb);
+ updateHandler.sendEmptyMessage(-1);
+ }
+ }, LISTENER_CYCLE_TIME);
+
+
+ if (!hostdb.savePortForward(pfb))
+ throw new SQLException("Could not save port forward");
+
+ updateHandler.sendEmptyMessage(-1);
+ } catch (Exception e) {
+ Log.e(TAG, "Could not update port forward", e);
+ // TODO Show failure dialog.
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null).create().show();
+
+ return true;
+ }
+ });
+
+ MenuItem delete = menu.add(R.string.portforward_delete);
+ delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // prompt user to make sure they really want this
+ new AlertDialog.Builder(PortForwardListActivity.this)
+ .setMessage(getString(R.string.delete_message, pfb.getNickname()))
+ .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ // Delete the port forward from the host if needed.
+ if (hostBridge != null)
+ hostBridge.removePortForward(pfb);
+
+ hostdb.deletePortForward(pfb);
+ } catch (Exception e) {
+ Log.e(TAG, "Could not delete port forward", e);
+ }
+
+ updateHandler.sendEmptyMessage(-1);
+ }
+ })
+ .setNegativeButton(R.string.delete_neg, null).create().show();
+
+ return true;
+ }
+ });
+ }
+
+ protected Handler updateHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ PortForwardListActivity.this.updateList();
+ }
+ };
+
+ protected void updateList() {
+ if (hostBridge != null) {
+ this.portForwards = hostBridge.getPortForwards();
+ } else {
+ if (this.hostdb == null) return;
+ this.portForwards = this.hostdb.getPortForwardsForHost(host);
+ }
+
+ PortForwardAdapter adapter = new PortForwardAdapter(this, portForwards);
+
+ this.setListAdapter(adapter);
+ }
+
+ class PortForwardAdapter extends ArrayAdapter<PortForwardBean> {
+ class ViewHolder {
+ public TextView nickname;
+ public TextView caption;
+ }
+
+ private List<PortForwardBean> portForwards;
+
+ public PortForwardAdapter(Context context, List<PortForwardBean> portForwards) {
+ super(context, R.layout.item_portforward, portForwards);
+
+ this.portForwards = portForwards;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.item_portforward, null, false);
+
+ holder = new ViewHolder();
+ holder.nickname = (TextView)convertView.findViewById(android.R.id.text1);
+ holder.caption = (TextView)convertView.findViewById(android.R.id.text2);
+
+ convertView.setTag(holder);
+ } else
+ holder = (ViewHolder) convertView.getTag();
+
+ PortForwardBean pfb = portForwards.get(position);
+ holder.nickname.setText(pfb.getNickname());
+ holder.caption.setText(pfb.getDescription());
+
+ if (hostBridge != null && !pfb.isEnabled()) {
+ holder.nickname.setPaintFlags(holder.nickname.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ holder.caption.setPaintFlags(holder.caption.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+
+ return convertView;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/PubkeyListActivity.java b/app/src/main/java/org/connectbot/PubkeyListActivity.java
new file mode 100644
index 0000000..be7a46f
--- /dev/null
+++ b/app/src/main/java/org/connectbot/PubkeyListActivity.java
@@ -0,0 +1,673 @@
+/*
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.EventListener;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+import org.openintents.intents.FileManagerIntents;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.IBinder;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TableRow;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.trilead.ssh2.crypto.Base64;
+import com.trilead.ssh2.crypto.PEMDecoder;
+import com.trilead.ssh2.crypto.PEMStructure;
+
+/**
+ * List public keys in database by nickname and describe their properties. Allow users to import,
+ * generate, rename, and delete key pairs.
+ *
+ * @author Kenny Root
+ */
+public class PubkeyListActivity extends ListActivity implements EventListener {
+ public final static String TAG = "ConnectBot.PubkeyListActivity";
+
+ private static final int MAX_KEYFILE_SIZE = 8192;
+ private static final int REQUEST_CODE_PICK_FILE = 1;
+
+ // Constants for AndExplorer's file picking intent
+ private static final String ANDEXPLORER_TITLE = "explorer_title";
+ private static final String MIME_TYPE_ANDEXPLORER_FILE = "vnd.android.cursor.dir/lysesoft.andexplorer.file";
+
+ protected PubkeyDatabase pubkeydb;
+ private List<PubkeyBean> pubkeys;
+
+ protected ClipboardManager clipboard;
+
+ protected LayoutInflater inflater = null;
+
+ protected TerminalManager bound = null;
+
+ private MenuItem onstartToggle = null;
+ private MenuItem confirmUse = null;
+
+ private ServiceConnection connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ bound = ((TerminalManager.TerminalBinder) service).getService();
+
+ // update our listview binder to find the service
+ updateList();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ bound = null;
+ updateList();
+ }
+ };
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ if(pubkeydb == null)
+ pubkeydb = new PubkeyDatabase(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ unbindService(connection);
+
+ if(pubkeydb != null) {
+ pubkeydb.close();
+ pubkeydb = null;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.act_pubkeylist);
+
+ this.setTitle(String.format("%s: %s",
+ getResources().getText(R.string.app_name),
+ getResources().getText(R.string.title_pubkey_list)));
+
+ // connect with hosts database and populate list
+ pubkeydb = new PubkeyDatabase(this);
+
+ updateList();
+
+ registerForContextMenu(getListView());
+
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
+ PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(position);
+ boolean loaded = bound.isKeyLoaded(pubkey.getNickname());
+
+ // handle toggling key in-memory on/off
+ if(loaded) {
+ bound.removeKey(pubkey.getNickname());
+ updateList();
+ } else {
+ handleAddKey(pubkey);
+ }
+
+ }
+ });
+
+ clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
+
+ inflater = LayoutInflater.from(this);
+ }
+
+ /**
+ * Read given file into memory as <code>byte[]</code>.
+ */
+ protected static byte[] readRaw(File file) throws Exception {
+ InputStream is = new FileInputStream(file);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ int bytesRead;
+ byte[] buffer = new byte[1024];
+ while ((bytesRead = is.read(buffer)) != -1) {
+ os.write(buffer, 0, bytesRead);
+ }
+
+ os.flush();
+ os.close();
+ is.close();
+
+ return os.toByteArray();
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem generatekey = menu.add(R.string.pubkey_generate);
+ generatekey.setIcon(android.R.drawable.ic_menu_manage);
+ generatekey.setIntent(new Intent(PubkeyListActivity.this, GeneratePubkeyActivity.class));
+
+ MenuItem importkey = menu.add(R.string.pubkey_import);
+ importkey.setIcon(android.R.drawable.ic_menu_upload);
+ importkey.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Uri sdcard = Uri.fromFile(Environment.getExternalStorageDirectory());
+ String pickerTitle = getString(R.string.pubkey_list_pick);
+
+ // Try to use OpenIntent's file browser to pick a file
+ Intent intent = new Intent(FileManagerIntents.ACTION_PICK_FILE);
+ intent.setData(sdcard);
+ intent.putExtra(FileManagerIntents.EXTRA_TITLE, pickerTitle);
+ intent.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok));
+
+ try {
+ startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
+ } catch (ActivityNotFoundException e) {
+ // If OI didn't work, try AndExplorer
+ intent = new Intent(Intent.ACTION_PICK);
+ intent.setDataAndType(sdcard, MIME_TYPE_ANDEXPLORER_FILE);
+ intent.putExtra(ANDEXPLORER_TITLE, pickerTitle);
+
+ try {
+ startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
+ } catch (ActivityNotFoundException e1) {
+ pickFileSimple();
+ }
+ }
+
+ return true;
+ }
+ });
+
+ return true;
+ }
+
+ protected void handleAddKey(final PubkeyBean pubkey) {
+ if (pubkey.isEncrypted()) {
+ final View view = inflater.inflate(R.layout.dia_password, null);
+ final EditText passwordField = (EditText)view.findViewById(android.R.id.text1);
+
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setView(view)
+ .setPositiveButton(R.string.pubkey_unlock, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ handleAddKey(pubkey, passwordField.getText().toString());
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null).create().show();
+ } else {
+ handleAddKey(pubkey, null);
+ }
+ }
+
+ protected void handleAddKey(PubkeyBean keybean, String password) {
+ KeyPair pair = null;
+ if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(keybean.getType())) {
+ // load specific key using pem format
+ try {
+ pair = PEMDecoder.decode(new String(keybean.getPrivateKey()).toCharArray(), password);
+ } catch(Exception e) {
+ String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname());
+ Log.e(TAG, message, e);
+ Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ // load using internal generated format
+ try {
+ PrivateKey privKey = PubkeyUtils.decodePrivate(keybean.getPrivateKey(), keybean.getType(), password);
+ PublicKey pubKey = PubkeyUtils.decodePublic(keybean.getPublicKey(), keybean.getType());
+ Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
+
+ pair = new KeyPair(pubKey, privKey);
+ } catch (Exception e) {
+ String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname());
+ Log.e(TAG, message, e);
+ Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show();
+ return;
+ }
+ }
+
+ if (pair == null) {
+ return;
+ }
+
+ Log.d(TAG, String.format("Unlocked key '%s'", keybean.getNickname()));
+
+ // save this key in memory
+ bound.addKey(keybean, pair, true);
+
+ updateList();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ // Create menu to handle deleting and editing pubkey
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ final PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(info.position);
+
+ menu.setHeaderTitle(pubkey.getNickname());
+
+ // TODO: option load/unload key from in-memory list
+ // prompt for password as needed for passworded keys
+
+ // cant change password or clipboard imported keys
+ final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType());
+ final boolean loaded = bound.isKeyLoaded(pubkey.getNickname());
+
+ MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load);
+ load.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ if(loaded) {
+ bound.removeKey(pubkey.getNickname());
+ updateList();
+ } else {
+ handleAddKey(pubkey);
+ //bound.addKey(nickname, trileadKey);
+ }
+ return true;
+ }
+ });
+
+ onstartToggle = menu.add(R.string.pubkey_load_on_start);
+ onstartToggle.setEnabled(!pubkey.isEncrypted());
+ onstartToggle.setCheckable(true);
+ onstartToggle.setChecked(pubkey.isStartup());
+ onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // toggle onstart status
+ pubkey.setStartup(!pubkey.isStartup());
+ pubkeydb.savePubkey(pubkey);
+ updateList();
+ return true;
+ }
+ });
+
+ MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public);
+ copyPublicToClipboard.setEnabled(!imported);
+ copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ try {
+ PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
+ String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname());
+
+ clipboard.setText(openSSHPubkey);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ });
+
+ MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private);
+ copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported);
+ copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ try {
+ String data = null;
+
+ if (imported)
+ data = new String(pubkey.getPrivateKey());
+ else {
+ PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType());
+ data = PubkeyUtils.exportPEM(pk, null);
+ }
+
+ clipboard.setText(data);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ });
+
+ MenuItem changePassword = menu.add(R.string.pubkey_change_password);
+ changePassword.setEnabled(!imported);
+ changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false);
+ ((TableRow)changePasswordView.findViewById(R.id.old_password_prompt))
+ .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE);
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setView(changePasswordView)
+ .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String oldPassword = ((EditText)changePasswordView.findViewById(R.id.old_password)).getText().toString();
+ String password1 = ((EditText)changePasswordView.findViewById(R.id.password1)).getText().toString();
+ String password2 = ((EditText)changePasswordView.findViewById(R.id.password2)).getText().toString();
+
+ if (!password1.equals(password2)) {
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setMessage(R.string.alert_passwords_do_not_match_msg)
+ .setPositiveButton(android.R.string.ok, null)
+ .create().show();
+ return;
+ }
+
+ try {
+ if (!pubkey.changePassword(oldPassword, password1))
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setMessage(R.string.alert_wrong_password_msg)
+ .setPositiveButton(android.R.string.ok, null)
+ .create().show();
+ else {
+ pubkeydb.savePubkey(pubkey);
+ updateList();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Could not change private key password", e);
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setMessage(R.string.alert_key_corrupted_msg)
+ .setPositiveButton(android.R.string.ok, null)
+ .create().show();
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null).create().show();
+
+ return true;
+ }
+ });
+
+ confirmUse = menu.add(R.string.pubkey_confirm_use);
+ confirmUse.setCheckable(true);
+ confirmUse.setChecked(pubkey.isConfirmUse());
+ confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // toggle confirm use
+ pubkey.setConfirmUse(!pubkey.isConfirmUse());
+ pubkeydb.savePubkey(pubkey);
+ updateList();
+ return true;
+ }
+ });
+
+ MenuItem delete = menu.add(R.string.pubkey_delete);
+ delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // prompt user to make sure they really want this
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setMessage(getString(R.string.delete_message, pubkey.getNickname()))
+ .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+
+ // dont forget to remove from in-memory
+ if(loaded)
+ bound.removeKey(pubkey.getNickname());
+
+ // delete from backend database and update gui
+ pubkeydb.deletePubkey(pubkey);
+ updateList();
+ }
+ })
+ .setNegativeButton(R.string.delete_neg, null).create().show();
+
+ return true;
+ }
+ });
+
+ }
+
+ protected void updateList() {
+ if (pubkeydb == null) return;
+
+ pubkeys = pubkeydb.allPubkeys();
+ PubkeyAdapter adapter = new PubkeyAdapter(this, pubkeys);
+
+ this.setListAdapter(adapter);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_FILE:
+ if (resultCode == RESULT_OK && intent != null) {
+ Uri uri = intent.getData();
+ try {
+ if (uri != null) {
+ readKeyFromFile(new File(URI.create(uri.toString())));
+ } else {
+ String filename = intent.getDataString();
+ if (filename != null)
+ readKeyFromFile(new File(URI.create(filename)));
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Couldn't read from picked file", e);
+ }
+ }
+ break;
+ }
+ }
+
+ /**
+ * @param name
+ */
+ private void readKeyFromFile(File file) {
+ PubkeyBean pubkey = new PubkeyBean();
+
+ // find the exact file selected
+ pubkey.setNickname(file.getName());
+
+ if (file.length() > MAX_KEYFILE_SIZE) {
+ Toast.makeText(PubkeyListActivity.this,
+ R.string.pubkey_import_parse_problem,
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ // parse the actual key once to check if its encrypted
+ // then save original file contents into our database
+ try {
+ byte[] raw = readRaw(file);
+
+ String data = new String(raw);
+ if (data.startsWith(PubkeyUtils.PKCS8_START)) {
+ int start = data.indexOf(PubkeyUtils.PKCS8_START) + PubkeyUtils.PKCS8_START.length();
+ int end = data.indexOf(PubkeyUtils.PKCS8_END);
+
+ if (end > start) {
+ char[] encoded = data.substring(start, end - 1).toCharArray();
+ Log.d(TAG, "encoded: " + new String(encoded));
+ byte[] decoded = Base64.decode(encoded);
+
+ KeyPair kp = PubkeyUtils.recoverKeyPair(decoded);
+
+ pubkey.setType(kp.getPrivate().getAlgorithm());
+ pubkey.setPrivateKey(kp.getPrivate().getEncoded());
+ pubkey.setPublicKey(kp.getPublic().getEncoded());
+ } else {
+ Log.e(TAG, "Problem parsing PKCS#8 file; corrupt?");
+ Toast.makeText(PubkeyListActivity.this,
+ R.string.pubkey_import_parse_problem,
+ Toast.LENGTH_LONG).show();
+ }
+ } else {
+ PEMStructure struct = PEMDecoder.parsePEM(new String(raw).toCharArray());
+ pubkey.setEncrypted(PEMDecoder.isPEMEncrypted(struct));
+ pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED);
+ pubkey.setPrivateKey(raw);
+ }
+
+ // write new value into database
+ if (pubkeydb == null)
+ pubkeydb = new PubkeyDatabase(this);
+ pubkeydb.savePubkey(pubkey);
+
+ updateList();
+ } catch(Exception e) {
+ Log.e(TAG, "Problem parsing imported private key", e);
+ Toast.makeText(PubkeyListActivity.this, R.string.pubkey_import_parse_problem, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ /**
+ *
+ */
+ private void pickFileSimple() {
+ // build list of all files in sdcard root
+ final File sdcard = Environment.getExternalStorageDirectory();
+ Log.d(TAG, sdcard.toString());
+
+ // Don't show a dialog if the SD card is completely absent.
+ final String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)
+ && !Environment.MEDIA_MOUNTED.equals(state)) {
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setMessage(R.string.alert_sdcard_absent)
+ .setNegativeButton(android.R.string.cancel, null).create().show();
+ return;
+ }
+
+ List<String> names = new LinkedList<String>();
+ {
+ File[] files = sdcard.listFiles();
+ if (files != null) {
+ for(File file : sdcard.listFiles()) {
+ if(file.isDirectory()) continue;
+ names.add(file.getName());
+ }
+ }
+ }
+ Collections.sort(names);
+
+ final String[] namesList = names.toArray(new String[] {});
+ Log.d(TAG, names.toString());
+
+ // prompt user to select any file from the sdcard root
+ new AlertDialog.Builder(PubkeyListActivity.this)
+ .setTitle(R.string.pubkey_list_pick)
+ .setItems(namesList, new OnClickListener() {
+ public void onClick(DialogInterface arg0, int arg1) {
+ String name = namesList[arg1];
+
+ readKeyFromFile(new File(sdcard, name));
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null).create().show();
+ }
+
+ class PubkeyAdapter extends ArrayAdapter<PubkeyBean> {
+ private List<PubkeyBean> pubkeys;
+
+ class ViewHolder {
+ public TextView nickname;
+ public TextView caption;
+ public ImageView icon;
+ }
+
+ public PubkeyAdapter(Context context, List<PubkeyBean> pubkeys) {
+ super(context, R.layout.item_pubkey, pubkeys);
+
+ this.pubkeys = pubkeys;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.item_pubkey, null, false);
+
+ holder = new ViewHolder();
+
+ holder.nickname = (TextView) convertView.findViewById(android.R.id.text1);
+ holder.caption = (TextView) convertView.findViewById(android.R.id.text2);
+ holder.icon = (ImageView) convertView.findViewById(android.R.id.icon1);
+
+ convertView.setTag(holder);
+ } else
+ holder = (ViewHolder) convertView.getTag();
+
+ PubkeyBean pubkey = pubkeys.get(position);
+ holder.nickname.setText(pubkey.getNickname());
+
+ boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType());
+
+ if (imported) {
+ try {
+ PEMStructure struct = PEMDecoder.parsePEM(new String(pubkey.getPrivateKey()).toCharArray());
+ String type = (struct.pemType == PEMDecoder.PEM_RSA_PRIVATE_KEY) ? "RSA" : "DSA";
+ holder.caption.setText(String.format("%s unknown-bit", type));
+ } catch (IOException e) {
+ Log.e(TAG, "Error decoding IMPORTED public key at " + pubkey.getId(), e);
+ }
+ } else {
+ try {
+ holder.caption.setText(pubkey.getDescription());
+ } catch (Exception e) {
+ Log.e(TAG, "Error decoding public key at " + pubkey.getId(), e);
+ holder.caption.setText(R.string.pubkey_unknown_format);
+ }
+ }
+
+ if (bound == null) {
+ holder.icon.setVisibility(View.GONE);
+ } else {
+ holder.icon.setVisibility(View.VISIBLE);
+
+ if (bound.isKeyLoaded(pubkey.getNickname()))
+ holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true);
+ else
+ holder.icon.setImageState(new int[] { }, true);
+ }
+
+ return convertView;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/SettingsActivity.java b/app/src/main/java/org/connectbot/SettingsActivity.java
new file mode 100644
index 0000000..460805d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/SettingsActivity.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import org.connectbot.util.PreferenceConstants;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+public class SettingsActivity extends PreferenceActivity {
+ private static final String TAG = "ConnectBot.Settings";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ try {
+ addPreferencesFromResource(R.xml.preferences);
+ } catch (ClassCastException e) {
+ Log.e(TAG, "Shared preferences are corrupt! Resetting to default values.");
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Blow away all the preferences
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.clear();
+ editor.commit();
+
+ PreferenceManager.setDefaultValues(this, R.xml.preferences, true);
+
+ // Since they were able to get to the Settings activity, they already agreed to the EULA
+ editor = preferences.edit();
+ editor.putBoolean(PreferenceConstants.EULA, true);
+ editor.commit();
+
+ addPreferencesFromResource(R.xml.preferences);
+ }
+
+ // TODO: add parse checking here to make sure we have integer value for scrollback
+
+ }
+
+}
diff --git a/app/src/main/java/org/connectbot/StrictModeSetup.java b/app/src/main/java/org/connectbot/StrictModeSetup.java
new file mode 100644
index 0000000..3a2000e
--- /dev/null
+++ b/app/src/main/java/org/connectbot/StrictModeSetup.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+import android.os.StrictMode;
+public class StrictModeSetup {
+ public static void run() {
+ StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/TerminalView.java b/app/src/main/java/org/connectbot/TerminalView.java
new file mode 100644
index 0000000..02683c2
--- /dev/null
+++ b/app/src/main/java/org/connectbot/TerminalView.java
@@ -0,0 +1,452 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.bean.SelectionArea;
+import org.connectbot.service.FontSizeChangedListener;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalKeyListener;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelXorXfermode;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
+import de.mud.terminal.VDUBuffer;
+
+/**
+ * User interface {@link View} for showing a TerminalBridge in an
+ * {@link Activity}. Handles drawing bitmap updates and passing keystrokes down
+ * to terminal.
+ *
+ * @author jsharkey
+ */
+public class TerminalView extends View implements FontSizeChangedListener {
+
+ private final Context context;
+ public final TerminalBridge bridge;
+ private final Paint paint;
+ private final Paint cursorPaint;
+ private final Paint cursorStrokePaint;
+
+ // Cursor paints to distinguish modes
+ private Path ctrlCursor, altCursor, shiftCursor;
+ private RectF tempSrc, tempDst;
+ private Matrix scaleMatrix;
+ private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL;
+
+ private Toast notification = null;
+ private String lastNotification = null;
+ private volatile boolean notifications = true;
+
+ // Related to Accessibility Features
+ private boolean mAccessibilityInitialized = false;
+ private boolean mAccessibilityActive = true;
+ private Object[] mAccessibilityLock = new Object[0];
+ private StringBuffer mAccessibilityBuffer;
+ private Pattern mControlCodes = null;
+ private Matcher mCodeMatcher = null;
+ private AccessibilityEventSender mEventSender = null;
+
+ private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K";
+ private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]";
+
+ private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000;
+ private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
+ private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
+
+ public TerminalView(Context context, TerminalBridge bridge) {
+ super(context);
+
+ this.context = context;
+ this.bridge = bridge;
+ paint = new Paint();
+
+ setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ cursorPaint = new Paint();
+ cursorPaint.setColor(bridge.color[bridge.defaultFg]);
+ cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
+ cursorPaint.setAntiAlias(true);
+
+ cursorStrokePaint = new Paint(cursorPaint);
+ cursorStrokePaint.setStrokeWidth(0.1f);
+ cursorStrokePaint.setStyle(Paint.Style.STROKE);
+
+ /*
+ * Set up our cursor indicators on a 1x1 Path object which we can later
+ * transform to our character width and height
+ */
+ // TODO make this into a resource somehow
+ shiftCursor = new Path();
+ shiftCursor.lineTo(0.5f, 0.33f);
+ shiftCursor.lineTo(1.0f, 0.0f);
+
+ altCursor = new Path();
+ altCursor.moveTo(0.0f, 1.0f);
+ altCursor.lineTo(0.5f, 0.66f);
+ altCursor.lineTo(1.0f, 1.0f);
+
+ ctrlCursor = new Path();
+ ctrlCursor.moveTo(0.0f, 0.25f);
+ ctrlCursor.lineTo(1.0f, 0.5f);
+ ctrlCursor.lineTo(0.0f, 0.75f);
+
+ // For creating the transform when the terminal resizes
+ tempSrc = new RectF();
+ tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f);
+ tempDst = new RectF();
+ scaleMatrix = new Matrix();
+
+ bridge.addFontSizeChangedListener(this);
+
+ // connect our view up to the bridge
+ setOnKeyListener(bridge.getKeyHandler());
+
+ mAccessibilityBuffer = new StringBuffer();
+
+ // Enable accessibility features if a screen reader is active.
+ new AccessibilityStateTester().execute((Void) null);
+ }
+
+ public void destroy() {
+ // tell bridge to destroy its bitmap
+ bridge.parentDestroyed();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ bridge.parentChanged(this);
+
+ scaleCursors();
+ }
+
+ public void onFontSizeChanged(float size) {
+ scaleCursors();
+ }
+
+ private void scaleCursors() {
+ // Create a scale matrix to scale our 1x1 representation of the cursor
+ tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight);
+ scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if(bridge.bitmap != null) {
+ // draw the bitmap
+ bridge.onDraw();
+
+ // draw the bridge bitmap if it exists
+ canvas.drawBitmap(bridge.bitmap, 0, 0, paint);
+
+ // also draw cursor if visible
+ if (bridge.buffer.isCursorVisible()) {
+ int cursorColumn = bridge.buffer.getCursorColumn();
+ final int cursorRow = bridge.buffer.getCursorRow();
+
+ final int columns = bridge.buffer.getColumns();
+
+ if (cursorColumn == columns)
+ cursorColumn = columns - 1;
+
+ if (cursorColumn < 0 || cursorRow < 0)
+ return;
+
+ int currentAttribute = bridge.buffer.getAttributes(
+ cursorColumn, cursorRow);
+ boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0;
+
+ int x = cursorColumn * bridge.charWidth;
+ int y = (bridge.buffer.getCursorRow()
+ + bridge.buffer.screenBase - bridge.buffer.windowBase)
+ * bridge.charHeight;
+
+ // Save the current clip and translation
+ canvas.save();
+
+ canvas.translate(x, y);
+ canvas.clipRect(0, 0,
+ bridge.charWidth * (onWideCharacter ? 2 : 1),
+ bridge.charHeight);
+ canvas.drawPaint(cursorPaint);
+
+ final int deadKey = bridge.getKeyHandler().getDeadKey();
+ if (deadKey != 0) {
+ canvas.drawText(new char[] { (char)deadKey }, 0, 1, 0, 0, cursorStrokePaint);
+ }
+
+ // Make sure we scale our decorations to the correct size.
+ canvas.concat(scaleMatrix);
+
+ int metaState = bridge.getKeyHandler().getMetaState();
+
+ if ((metaState & TerminalKeyListener.OUR_SHIFT_ON) != 0)
+ canvas.drawPath(shiftCursor, cursorStrokePaint);
+ else if ((metaState & TerminalKeyListener.OUR_SHIFT_LOCK) != 0)
+ canvas.drawPath(shiftCursor, cursorPaint);
+
+ if ((metaState & TerminalKeyListener.OUR_ALT_ON) != 0)
+ canvas.drawPath(altCursor, cursorStrokePaint);
+ else if ((metaState & TerminalKeyListener.OUR_ALT_LOCK) != 0)
+ canvas.drawPath(altCursor, cursorPaint);
+
+ if ((metaState & TerminalKeyListener.OUR_CTRL_ON) != 0)
+ canvas.drawPath(ctrlCursor, cursorStrokePaint);
+ else if ((metaState & TerminalKeyListener.OUR_CTRL_LOCK) != 0)
+ canvas.drawPath(ctrlCursor, cursorPaint);
+
+ // Restore previous clip region
+ canvas.restore();
+ }
+
+ // draw any highlighted area
+ if (bridge.isSelectingForCopy()) {
+ SelectionArea area = bridge.getSelectionArea();
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipRect(
+ area.getLeft() * bridge.charWidth,
+ area.getTop() * bridge.charHeight,
+ (area.getRight() + 1) * bridge.charWidth,
+ (area.getBottom() + 1) * bridge.charHeight
+ );
+ canvas.drawPaint(cursorPaint);
+ canvas.restore();
+ }
+ }
+ }
+
+ public void notifyUser(String message) {
+ if (!notifications)
+ return;
+
+ if (notification != null) {
+ // Don't keep telling the user the same thing.
+ if (lastNotification != null && lastNotification.equals(message))
+ return;
+
+ notification.setText(message);
+ notification.show();
+ } else {
+ notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
+ notification.show();
+ }
+
+ lastNotification = message;
+ }
+
+ /**
+ * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
+ * @param width
+ * @param height
+ */
+ public void forceSize(int width, int height) {
+ bridge.resizeComputed(width, height, getWidth(), getHeight());
+ }
+
+ /**
+ * Sets the ability for the TerminalView to display Toast notifications to the user.
+ * @param value whether to enable notifications or not
+ */
+ public void setNotifications(boolean value) {
+ notifications = value;
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return true;
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ outAttrs.imeOptions |=
+ EditorInfo.IME_FLAG_NO_EXTRACT_UI |
+ EditorInfo.IME_FLAG_NO_ENTER_ACTION |
+ EditorInfo.IME_ACTION_NONE;
+ outAttrs.inputType = EditorInfo.TYPE_NULL;
+ return new BaseInputConnection(this, false) {
+ @Override
+ public boolean deleteSurroundingText (int leftLength, int rightLength) {
+ if (rightLength == 0 && leftLength == 0) {
+ return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ }
+ for (int i = 0; i < leftLength; i++) {
+ this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ }
+ // TODO: forward delete
+ return true;
+ }
+ };
+ }
+
+ public void propagateConsoleText(char[] rawText, int length) {
+ if (mAccessibilityActive) {
+ synchronized (mAccessibilityLock) {
+ mAccessibilityBuffer.append(rawText, 0, length);
+ }
+
+ if (mAccessibilityInitialized) {
+ if (mEventSender != null) {
+ removeCallbacks(mEventSender);
+ } else {
+ mEventSender = new AccessibilityEventSender();
+ }
+
+ postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
+ }
+ }
+ }
+
+ private class AccessibilityEventSender implements Runnable {
+ public void run() {
+ synchronized (mAccessibilityLock) {
+ if (mCodeMatcher == null) {
+ mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer);
+ } else {
+ mCodeMatcher.reset(mAccessibilityBuffer);
+ }
+
+ // Strip all control codes out.
+ mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" "));
+
+ // Apply Backspaces using backspace character sequence
+ int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
+ while (i != -1) {
+ mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i
+ + BACKSPACE_CODE.length(), "");
+ i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE);
+ }
+
+ if (mAccessibilityBuffer.length() > 0) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+ event.setFromIndex(0);
+ event.setAddedCount(mAccessibilityBuffer.length());
+ event.getText().add(mAccessibilityBuffer);
+
+ sendAccessibilityEventUnchecked(event);
+ mAccessibilityBuffer.setLength(0);
+ }
+ }
+ }
+ }
+
+ private class AccessibilityStateTester extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ /*
+ * Presumably if the accessibility manager is not enabled, we don't
+ * need to send accessibility events.
+ */
+ final AccessibilityManager accessibility = (AccessibilityManager) context
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!accessibility.isEnabled()) {
+ return false;
+ }
+
+ /*
+ * Restrict the set of intents to only accessibility services that
+ * have the category FEEDBACK_SPOKEN (aka, screen readers).
+ */
+ final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION);
+ screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY);
+
+ final ContentResolver cr = context.getContentResolver();
+
+ final List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(
+ screenReaderIntent, 0);
+
+ boolean foundScreenReader = false;
+
+ final int N = screenReaders.size();
+ for (int i = 0; i < N; i++) {
+ final ResolveInfo screenReader = screenReaders.get(i);
+
+ /*
+ * All screen readers are expected to implement a content
+ * provider that responds to:
+ * content://<nameofpackage>.providers.StatusProvider
+ */
+ final Cursor cursor = cr.query(
+ Uri.parse("content://" + screenReader.serviceInfo.packageName
+ + ".providers.StatusProvider"), null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ /*
+ * These content providers use a special cursor that only has
+ * one element, an integer that is 1 if the screen reader is
+ * running.
+ */
+ final int status = cursor.getInt(0);
+
+ cursor.close();
+
+ if (status == 1) {
+ foundScreenReader = true;
+ break;
+ }
+ }
+ }
+
+ if (foundScreenReader) {
+ mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN);
+ }
+
+ return foundScreenReader;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mAccessibilityActive = result;
+
+ mAccessibilityInitialized = true;
+
+ if (result) {
+ mEventSender = new AccessibilityEventSender();
+ postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD);
+ } else {
+ mAccessibilityBuffer = null;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/WizardActivity.java b/app/src/main/java/org/connectbot/WizardActivity.java
new file mode 100644
index 0000000..35a60ca
--- /dev/null
+++ b/app/src/main/java/org/connectbot/WizardActivity.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+import org.connectbot.util.HelpTopicView;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ViewFlipper;
+
+/**
+ * Show a series of wizard-like steps to the user, which might include an EULA,
+ * program credits, and helpful hints.
+ *
+ * @author jsharkey
+ */
+public class WizardActivity extends Activity {
+ protected ViewFlipper flipper = null;
+ private Button next, prev;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.act_wizard);
+
+ this.flipper = (ViewFlipper) findViewById(R.id.wizard_flipper);
+
+ // inflate the layout for EULA step
+ LayoutInflater inflater = LayoutInflater.from(this);
+ this.flipper.addView(inflater.inflate(R.layout.wiz_eula, this.flipper, false));
+
+ // Add a view for each help topic we want the user to see.
+ String[] topics = getResources().getStringArray(R.array.list_wizard_topics);
+ for (String topic : topics) {
+ flipper.addView(new HelpTopicView(this).setTopic(topic));
+ }
+
+ next = (Button)this.findViewById(R.id.action_next);
+ next.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if(isLastDisplayed()) {
+ // user walked past end of wizard, so return okay
+ WizardActivity.this.setResult(Activity.RESULT_OK);
+ WizardActivity.this.finish();
+ } else {
+ // show next step and update buttons
+ flipper.showNext();
+ updateButtons();
+ }
+ }
+ });
+
+ prev = (Button)this.findViewById(R.id.action_prev);
+ prev.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if(isFirstDisplayed()) {
+ // user walked past beginning of wizard, so return that they cancelled
+ WizardActivity.this.setResult(Activity.RESULT_CANCELED);
+ WizardActivity.this.finish();
+ } else {
+ // show previous step and update buttons
+ flipper.showPrevious();
+ updateButtons();
+ }
+ }
+ });
+
+ this.updateButtons();
+ }
+
+ protected boolean isFirstDisplayed() {
+ return (flipper.getDisplayedChild() == 0);
+ }
+
+ protected boolean isLastDisplayed() {
+ return (flipper.getDisplayedChild() == flipper.getChildCount() - 1);
+ }
+
+ protected void updateButtons() {
+ boolean eula = (flipper.getDisplayedChild() == 0);
+
+ next.setText(eula ? getString(R.string.wizard_agree) : getString(R.string.wizard_next));
+ prev.setText(eula ? getString(R.string.delete_neg) : getString(R.string.wizard_back));
+ }
+}
diff --git a/app/src/main/java/org/connectbot/bean/AbstractBean.java b/app/src/main/java/org/connectbot/bean/AbstractBean.java
new file mode 100644
index 0000000..7f55785
--- /dev/null
+++ b/app/src/main/java/org/connectbot/bean/AbstractBean.java
@@ -0,0 +1,49 @@
+/*
+ * 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.bean;
+
+import java.util.Map.Entry;
+
+import org.connectbot.util.XmlBuilder;
+
+import android.content.ContentValues;
+
+/**
+ * @author Kenny Root
+ *
+ */
+abstract class AbstractBean {
+ public abstract ContentValues getValues();
+ public abstract String getBeanName();
+
+ public String toXML() {
+ XmlBuilder xml = new XmlBuilder();
+
+ xml.append(String.format("<%s>", getBeanName()));
+
+ ContentValues values = getValues();
+ for (Entry<String, Object> entry : values.valueSet()) {
+ Object value = entry.getValue();
+ if (value != null)
+ xml.append(entry.getKey(), value);
+ }
+ xml.append(String.format("</%s>", getBeanName()));
+
+ return xml.toString();
+ }
+}
diff --git a/app/src/main/java/org/connectbot/bean/HostBean.java b/app/src/main/java/org/connectbot/bean/HostBean.java
new file mode 100644
index 0000000..2fd7bfb
--- /dev/null
+++ b/app/src/main/java/org/connectbot/bean/HostBean.java
@@ -0,0 +1,317 @@
+/*
+ * 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.bean;
+
+import org.connectbot.util.HostDatabase;
+
+import android.content.ContentValues;
+import android.net.Uri;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HostBean extends AbstractBean {
+ public static final String BEAN_NAME = "host";
+
+ /* Database fields */
+ private long id = -1;
+ private String nickname = null;
+ private String username = null;
+ private String hostname = null;
+ private int port = 22;
+ private String protocol = "ssh";
+ private String hostKeyAlgo = null;
+ private byte[] hostKey = null;
+ private long lastConnect = -1;
+ private String color;
+ private boolean useKeys = true;
+ private String useAuthAgent = HostDatabase.AUTHAGENT_NO;
+ private String postLogin = null;
+ private long pubkeyId = -1;
+ private boolean wantSession = true;
+ private String delKey = HostDatabase.DELKEY_DEL;
+ private int fontSize = -1;
+ private boolean compression = false;
+ private String encoding = HostDatabase.ENCODING_DEFAULT;
+ private boolean stayConnected = false;
+
+ public HostBean() {
+
+ }
+
+ @Override
+ public String getBeanName() {
+ return BEAN_NAME;
+ }
+
+ public HostBean(String nickname, String protocol, String username, String hostname, int port) {
+ this.nickname = nickname;
+ this.protocol = protocol;
+ this.username = username;
+ this.hostname = hostname;
+ this.port = port;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setNickname(String nickname) {
+ this.nickname = nickname;
+ }
+ public String getNickname() {
+ return nickname;
+ }
+ public void setUsername(String username) {
+ this.username = username;
+ }
+ public String getUsername() {
+ return username;
+ }
+ public void setHostname(String hostname) {
+ this.hostname = hostname;
+ }
+ public String getHostname() {
+ return hostname;
+ }
+ public void setPort(int port) {
+ this.port = port;
+ }
+ public int getPort() {
+ return port;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setHostKeyAlgo(String hostKeyAlgo) {
+ this.hostKeyAlgo = hostKeyAlgo;
+ }
+ public String getHostKeyAlgo() {
+ return hostKeyAlgo;
+ }
+ public void setHostKey(byte[] hostKey) {
+ if (hostKey == null)
+ this.hostKey = null;
+ else
+ this.hostKey = hostKey.clone();
+ }
+ public byte[] getHostKey() {
+ if (hostKey == null)
+ return null;
+ else
+ return hostKey.clone();
+ }
+ public void setLastConnect(long lastConnect) {
+ this.lastConnect = lastConnect;
+ }
+ public long getLastConnect() {
+ return lastConnect;
+ }
+ public void setColor(String color) {
+ this.color = color;
+ }
+ public String getColor() {
+ return color;
+ }
+ public void setUseKeys(boolean useKeys) {
+ this.useKeys = useKeys;
+ }
+ public boolean getUseKeys() {
+ return useKeys;
+ }
+ public void setUseAuthAgent(String useAuthAgent) {
+ this.useAuthAgent = useAuthAgent;
+ }
+ public String getUseAuthAgent() {
+ return useAuthAgent;
+ }
+ public void setPostLogin(String postLogin) {
+ this.postLogin = postLogin;
+ }
+ public String getPostLogin() {
+ return postLogin;
+ }
+ public void setPubkeyId(long pubkeyId) {
+ this.pubkeyId = pubkeyId;
+ }
+ public long getPubkeyId() {
+ return pubkeyId;
+ }
+ public void setWantSession(boolean wantSession) {
+ this.wantSession = wantSession;
+ }
+ public boolean getWantSession() {
+ return wantSession;
+ }
+ public void setDelKey(String delKey) {
+ this.delKey = delKey;
+ }
+ public String getDelKey() {
+ return delKey;
+ }
+ public void setFontSize(int fontSize) {
+ this.fontSize = fontSize;
+ }
+ public int getFontSize() {
+ return fontSize;
+ }
+ public void setCompression(boolean compression) {
+ this.compression = compression;
+ }
+ public boolean getCompression() {
+ return compression;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ public void setStayConnected(boolean stayConnected) {
+ this.stayConnected = stayConnected;
+ }
+
+ public boolean getStayConnected() {
+ return stayConnected;
+ }
+
+ public String getDescription() {
+ String description = String.format("%s@%s", username, hostname);
+
+ if (port != 22)
+ description += String.format(":%d", port);
+
+ return description;
+ }
+
+ @Override
+ public ContentValues getValues() {
+ ContentValues values = new ContentValues();
+
+ values.put(HostDatabase.FIELD_HOST_NICKNAME, nickname);
+ values.put(HostDatabase.FIELD_HOST_PROTOCOL, protocol);
+ values.put(HostDatabase.FIELD_HOST_USERNAME, username);
+ values.put(HostDatabase.FIELD_HOST_HOSTNAME, hostname);
+ values.put(HostDatabase.FIELD_HOST_PORT, port);
+ values.put(HostDatabase.FIELD_HOST_HOSTKEYALGO, hostKeyAlgo);
+ values.put(HostDatabase.FIELD_HOST_HOSTKEY, hostKey);
+ values.put(HostDatabase.FIELD_HOST_LASTCONNECT, lastConnect);
+ values.put(HostDatabase.FIELD_HOST_COLOR, color);
+ values.put(HostDatabase.FIELD_HOST_USEKEYS, Boolean.toString(useKeys));
+ values.put(HostDatabase.FIELD_HOST_USEAUTHAGENT, useAuthAgent);
+ values.put(HostDatabase.FIELD_HOST_POSTLOGIN, postLogin);
+ values.put(HostDatabase.FIELD_HOST_PUBKEYID, pubkeyId);
+ values.put(HostDatabase.FIELD_HOST_WANTSESSION, Boolean.toString(wantSession));
+ values.put(HostDatabase.FIELD_HOST_DELKEY, delKey);
+ values.put(HostDatabase.FIELD_HOST_FONTSIZE, fontSize);
+ values.put(HostDatabase.FIELD_HOST_COMPRESSION, Boolean.toString(compression));
+ values.put(HostDatabase.FIELD_HOST_ENCODING, encoding);
+ values.put(HostDatabase.FIELD_HOST_STAYCONNECTED, stayConnected);
+
+ return values;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof HostBean))
+ return false;
+
+ HostBean host = (HostBean)o;
+
+ if (id != -1 && host.getId() != -1)
+ return host.getId() == id;
+
+ if (nickname == null) {
+ if (host.getNickname() != null)
+ return false;
+ } else if (!nickname.equals(host.getNickname()))
+ return false;
+
+ if (protocol == null) {
+ if (host.getProtocol() != null)
+ return false;
+ } else if (!protocol.equals(host.getProtocol()))
+ return false;
+
+ if (username == null) {
+ if (host.getUsername() != null)
+ return false;
+ } else if (!username.equals(host.getUsername()))
+ return false;
+
+ if (hostname == null) {
+ if (host.getHostname() != null)
+ return false;
+ } else if (!hostname.equals(host.getHostname()))
+ return false;
+
+ if (port != host.getPort())
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+
+ if (id != -1)
+ return (int)id;
+
+ hash = 31 * hash + (null == nickname ? 0 : nickname.hashCode());
+ hash = 31 * hash + (null == protocol ? 0 : protocol.hashCode());
+ hash = 31 * hash + (null == username ? 0 : username.hashCode());
+ hash = 31 * hash + (null == hostname ? 0 : hostname.hashCode());
+ hash = 31 * hash + port;
+
+ return hash;
+ }
+
+ /**
+ * @return URI identifying this HostBean
+ */
+ public Uri getUri() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(protocol)
+ .append("://");
+
+ if (username != null)
+ sb.append(Uri.encode(username))
+ .append('@');
+
+ sb.append(Uri.encode(hostname))
+ .append(':')
+ .append(port)
+ .append("/#")
+ .append(nickname);
+ return Uri.parse(sb.toString());
+ }
+
+}
diff --git a/app/src/main/java/org/connectbot/bean/PortForwardBean.java b/app/src/main/java/org/connectbot/bean/PortForwardBean.java
new file mode 100644
index 0000000..2bdaf20
--- /dev/null
+++ b/app/src/main/java/org/connectbot/bean/PortForwardBean.java
@@ -0,0 +1,239 @@
+/*
+ * 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.bean;
+
+import org.connectbot.util.HostDatabase;
+
+import android.content.ContentValues;
+
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class PortForwardBean extends AbstractBean {
+ public static final String BEAN_NAME = "portforward";
+
+ /* Database fields */
+ private long id = -1;
+ private long hostId = -1;
+ private String nickname = null;
+ private String type = null;
+ private int sourcePort = -1;
+ private String destAddr = null;
+ private int destPort = -1;
+
+ /* Transient values */
+ private boolean enabled = false;
+ private Object identifier = null;
+
+ /**
+ * @param id database ID of port forward
+ * @param nickname Nickname to use to identify port forward
+ * @param type One of the port forward types from {@link HostDatabase}
+ * @param sourcePort Source port number
+ * @param destAddr Destination hostname or IP address
+ * @param destPort Destination port number
+ */
+ public PortForwardBean(long id, long hostId, String nickname, String type, int sourcePort, String destAddr, int destPort) {
+ this.id = id;
+ this.hostId = hostId;
+ this.nickname = nickname;
+ this.type = type;
+ this.sourcePort = sourcePort;
+ this.destAddr = destAddr;
+ this.destPort = destPort;
+ }
+
+ /**
+ * @param type One of the port forward types from {@link HostDatabase}
+ * @param source Source port number
+ * @param dest Destination is "host:port" format
+ */
+ public PortForwardBean(long hostId, String nickname, String type, String source, String dest) {
+ this.hostId = hostId;
+ this.nickname = nickname;
+ this.type = type;
+ this.sourcePort = Integer.parseInt(source);
+
+ setDest(dest);
+ }
+
+ public String getBeanName() {
+ return BEAN_NAME;
+ }
+
+ /**
+ * @param id the id to set
+ */
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * @return the id
+ */
+ public long getId() {
+ return id;
+ }
+
+ /**
+ * @param nickname the nickname to set
+ */
+ public void setNickname(String nickname) {
+ this.nickname = nickname;
+ }
+
+ /**
+ * @return the nickname
+ */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /**
+ * @param type the type to set
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * @return the type
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * @param sourcePort the sourcePort to set
+ */
+ public void setSourcePort(int sourcePort) {
+ this.sourcePort = sourcePort;
+ }
+
+ /**
+ * @return the sourcePort
+ */
+ public int getSourcePort() {
+ return sourcePort;
+ }
+
+ /**
+ * @param dest The destination in "host:port" format
+ */
+ public final void setDest(String dest) {
+ String[] destSplit = dest.split(":");
+ this.destAddr = destSplit[0];
+ if (destSplit.length > 1)
+ this.destPort = Integer.parseInt(destSplit[1]);
+ }
+
+ /**
+ * @param destAddr the destAddr to set
+ */
+ public void setDestAddr(String destAddr) {
+ this.destAddr = destAddr;
+ }
+
+ /**
+ * @return the destAddr
+ */
+ public String getDestAddr() {
+ return destAddr;
+ }
+
+ /**
+ * @param destPort the destPort to set
+ */
+ public void setDestPort(int destPort) {
+ this.destPort = destPort;
+ }
+
+ /**
+ * @return the destPort
+ */
+ public int getDestPort() {
+ return destPort;
+ }
+
+ /**
+ * @param enabled the enabled to set
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * @return the enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * @param identifier the identifier of this particular type to set
+ */
+ public void setIdentifier(Object identifier) {
+ this.identifier = identifier;
+ }
+
+ /**
+ * @return the identifier used by this particular type
+ */
+ public Object getIdentifier() {
+ return identifier;
+ }
+
+ /**
+ * @return human readable description of the port forward
+ */
+ public CharSequence getDescription() {
+ String description = "Unknown type";
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(type)) {
+ description = String.format("Local port %d to %s:%d", sourcePort, destAddr, destPort);
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(type)) {
+ description = String.format("Remote port %d to %s:%d", sourcePort, destAddr, destPort);
+/* I don't think we need the SOCKS4 type.
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC4.equals(type)) {
+ description = String.format("Dynamic port %d (SOCKS4)", sourcePort);
+*/
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(type)) {
+ description = String.format("Dynamic port %d (SOCKS)", sourcePort);
+ }
+
+ return description;
+ }
+
+ /**
+ * @return
+ */
+ public ContentValues getValues() {
+ ContentValues values = new ContentValues();
+
+ values.put(HostDatabase.FIELD_PORTFORWARD_HOSTID, hostId);
+ values.put(HostDatabase.FIELD_PORTFORWARD_NICKNAME, nickname);
+ values.put(HostDatabase.FIELD_PORTFORWARD_TYPE, type);
+ values.put(HostDatabase.FIELD_PORTFORWARD_SOURCEPORT, sourcePort);
+ values.put(HostDatabase.FIELD_PORTFORWARD_DESTADDR, destAddr);
+ values.put(HostDatabase.FIELD_PORTFORWARD_DESTPORT, destPort);
+
+ return values;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/bean/PubkeyBean.java b/app/src/main/java/org/connectbot/bean/PubkeyBean.java
new file mode 100644
index 0000000..656c6af
--- /dev/null
+++ b/app/src/main/java/org/connectbot/bean/PubkeyBean.java
@@ -0,0 +1,234 @@
+/*
+ * 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.bean;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.content.ContentValues;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class PubkeyBean extends AbstractBean {
+ public static final String BEAN_NAME = "pubkey";
+
+ private static final String KEY_TYPE_RSA = "RSA";
+
+ private static final String KEY_TYPE_DSA = "DSA";
+
+ private static final String KEY_TYPE_EC = "EC";
+
+ /* Database fields */
+ private long id;
+ private String nickname;
+ private String type;
+ private byte[] privateKey;
+ private byte[] publicKey;
+ private boolean encrypted = false;
+ private boolean startup = false;
+ private boolean confirmUse = false;
+ private int lifetime = 0;
+
+ /* Transient values */
+ private transient boolean unlocked = false;
+ private transient Object unlockedPrivate = null;
+ private transient String description;
+
+ @Override
+ public String getBeanName() {
+ return BEAN_NAME;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setNickname(String nickname) {
+ this.nickname = nickname;
+ }
+
+ public String getNickname() {
+ return nickname;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setPrivateKey(byte[] privateKey) {
+ if (privateKey == null)
+ this.privateKey = null;
+ else
+ this.privateKey = privateKey.clone();
+ }
+
+ public byte[] getPrivateKey() {
+ if (privateKey == null)
+ return null;
+ else
+ return privateKey.clone();
+ }
+
+ public void setPublicKey(byte[] encoded) {
+ if (encoded == null)
+ publicKey = null;
+ else
+ publicKey = encoded.clone();
+ }
+
+ public byte[] getPublicKey() {
+ if (publicKey == null)
+ return null;
+ else
+ return publicKey.clone();
+ }
+
+ public void setEncrypted(boolean encrypted) {
+ this.encrypted = encrypted;
+ }
+
+ public boolean isEncrypted() {
+ return encrypted;
+ }
+
+ public void setStartup(boolean startup) {
+ this.startup = startup;
+ }
+
+ public boolean isStartup() {
+ return startup;
+ }
+
+ public void setConfirmUse(boolean confirmUse) {
+ this.confirmUse = confirmUse;
+ }
+
+ public boolean isConfirmUse() {
+ return confirmUse;
+ }
+
+ public void setLifetime(int lifetime) {
+ this.lifetime = lifetime;
+ }
+
+ public int getLifetime() {
+ return lifetime;
+ }
+
+ public void setUnlocked(boolean unlocked) {
+ this.unlocked = unlocked;
+ }
+
+ public boolean isUnlocked() {
+ return unlocked;
+ }
+
+ public void setUnlockedPrivate(Object unlockedPrivate) {
+ this.unlockedPrivate = unlockedPrivate;
+ }
+
+ public Object getUnlockedPrivate() {
+ return unlockedPrivate;
+ }
+
+ public String getDescription() {
+ if (description == null) {
+ final StringBuilder sb = new StringBuilder();
+ try {
+ final PublicKey pubKey = PubkeyUtils.decodePublic(privateKey, type);
+ if (PubkeyDatabase.KEY_TYPE_RSA.equals(type)) {
+ int bits = ((RSAPublicKey) pubKey).getModulus().bitLength();
+ sb.append("RSA ");
+ sb.append(bits);
+ sb.append("-bit");
+ } else if (PubkeyDatabase.KEY_TYPE_DSA.equals(type)) {
+ sb.append("DSA 1024-bit");
+ } else if (PubkeyDatabase.KEY_TYPE_EC.equals(type)) {
+ int bits = ((ECPublicKey) pubKey).getParams().getCurve().getField()
+ .getFieldSize();
+ sb.append("EC ");
+ sb.append(bits);
+ sb.append("-bit");
+ } else {
+ sb.append("Unknown Key Type");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ sb.append("Unknown Key Type");
+ } catch (InvalidKeySpecException e) {
+ sb.append("Unknown Key Type");
+ }
+
+ if (encrypted)
+ sb.append(" (encrypted)");
+
+ description = sb.toString();
+ }
+ return description;
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.bean.AbstractBean#getValues()
+ */
+ @Override
+ public ContentValues getValues() {
+ ContentValues values = new ContentValues();
+
+ values.put(PubkeyDatabase.FIELD_PUBKEY_NICKNAME, nickname);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_TYPE, type);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_PRIVATE, privateKey);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_PUBLIC, publicKey);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED, encrypted ? 1 : 0);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_STARTUP, startup ? 1 : 0);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_CONFIRMUSE, confirmUse ? 1 : 0);
+ values.put(PubkeyDatabase.FIELD_PUBKEY_LIFETIME, lifetime);
+
+ return values;
+ }
+
+ public boolean changePassword(String oldPassword, String newPassword) throws Exception {
+ PrivateKey priv;
+
+ try {
+ priv = PubkeyUtils.decodePrivate(getPrivateKey(), getType(), oldPassword);
+ } catch (Exception e) {
+ return false;
+ }
+
+ setPrivateKey(PubkeyUtils.getEncodedPrivate(priv, newPassword));
+ setEncrypted(newPassword.length() > 0);
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/bean/SelectionArea.java b/app/src/main/java/org/connectbot/bean/SelectionArea.java
new file mode 100644
index 0000000..4e6207d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/bean/SelectionArea.java
@@ -0,0 +1,201 @@
+/*
+ * 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.bean;
+
+import de.mud.terminal.VDUBuffer;
+
+/**
+ * @author Kenny Root
+ * Keep track of a selection area for the terminal copying mechanism.
+ * If the orientation is flipped one way, swap the bottom and top or
+ * left and right to keep it in the correct orientation.
+ */
+public class SelectionArea {
+ private int top;
+ private int bottom;
+ private int left;
+ private int right;
+ private int maxColumns;
+ private int maxRows;
+ private boolean selectingOrigin;
+
+ public SelectionArea() {
+ reset();
+ }
+
+ public final void reset() {
+ top = left = bottom = right = 0;
+ selectingOrigin = true;
+ }
+
+ /**
+ * @param columns
+ * @param rows
+ */
+ public void setBounds(int columns, int rows) {
+ maxColumns = columns - 1;
+ maxRows = rows - 1;
+ }
+
+ private int checkBounds(int value, int max) {
+ if (value < 0)
+ return 0;
+ else if (value > max)
+ return max;
+ else
+ return value;
+ }
+
+ public boolean isSelectingOrigin() {
+ return selectingOrigin;
+ }
+
+ public void finishSelectingOrigin() {
+ selectingOrigin = false;
+ }
+
+ public void decrementRow() {
+ if (selectingOrigin)
+ setTop(top - 1);
+ else
+ setBottom(bottom - 1);
+ }
+
+ public void incrementRow() {
+ if (selectingOrigin)
+ setTop(top + 1);
+ else
+ setBottom(bottom + 1);
+ }
+
+ public void setRow(int row) {
+ if (selectingOrigin)
+ setTop(row);
+ else
+ setBottom(row);
+ }
+
+ private void setTop(int top) {
+ this.top = bottom = checkBounds(top, maxRows);
+ }
+
+ public int getTop() {
+ return Math.min(top, bottom);
+ }
+
+ private void setBottom(int bottom) {
+ this.bottom = checkBounds(bottom, maxRows);
+ }
+
+ public int getBottom() {
+ return Math.max(top, bottom);
+ }
+
+ public void decrementColumn() {
+ if (selectingOrigin)
+ setLeft(left - 1);
+ else
+ setRight(right - 1);
+ }
+
+ public void incrementColumn() {
+ if (selectingOrigin)
+ setLeft(left + 1);
+ else
+ setRight(right + 1);
+ }
+
+ public void setColumn(int column) {
+ if (selectingOrigin)
+ setLeft(column);
+ else
+ setRight(column);
+ }
+
+ private void setLeft(int left) {
+ this.left = right = checkBounds(left, maxColumns);
+ }
+
+ public int getLeft() {
+ return Math.min(left, right);
+ }
+
+ private void setRight(int right) {
+ this.right = checkBounds(right, maxColumns);
+ }
+
+ public int getRight() {
+ return Math.max(left, right);
+ }
+
+ public String copyFrom(VDUBuffer vb) {
+ int size = (getRight() - getLeft() + 1) * (getBottom() - getTop() + 1);
+
+ StringBuffer buffer = new StringBuffer(size);
+
+ for(int y = getTop(); y <= getBottom(); y++) {
+ int lastNonSpace = buffer.length();
+
+ for (int x = getLeft(); x <= getRight(); x++) {
+ // only copy printable chars
+ char c = vb.getChar(x, y);
+
+ if (!Character.isDefined(c) ||
+ (Character.isISOControl(c) && c != '\t'))
+ c = ' ';
+
+ if (c != ' ')
+ lastNonSpace = buffer.length();
+
+ buffer.append(c);
+ }
+
+ // Don't leave a bunch of spaces in our copy buffer.
+ if (buffer.length() > lastNonSpace)
+ buffer.delete(lastNonSpace + 1, buffer.length());
+
+ if (y != bottom)
+ buffer.append("\n");
+ }
+
+ return buffer.toString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+
+ buffer.append("SelectionArea[top=");
+ buffer.append(top);
+ buffer.append(", bottom=");
+ buffer.append(bottom);
+ buffer.append(", left=");
+ buffer.append(left);
+ buffer.append(", right=");
+ buffer.append(right);
+ buffer.append(", maxColumns=");
+ buffer.append(maxColumns);
+ buffer.append(", maxRows=");
+ buffer.append(maxRows);
+ buffer.append(", isSelectingOrigin=");
+ buffer.append(isSelectingOrigin());
+ buffer.append("]");
+
+ return buffer.toString();
+ }
+}
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();
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/AbsTransport.java b/app/src/main/java/org/connectbot/transport/AbsTransport.java
new file mode 100644
index 0000000..18397ea
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/AbsTransport.java
@@ -0,0 +1,254 @@
+/*
+ * 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.transport;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class AbsTransport {
+ HostBean host;
+ TerminalBridge bridge;
+ TerminalManager manager;
+
+ String emulation;
+
+ public AbsTransport() {}
+
+ public AbsTransport(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ this.host = host;
+ this.bridge = bridge;
+ this.manager = manager;
+ }
+
+ /**
+ * @return protocol part of the URI
+ */
+ public static String getProtocolName() {
+ return "unknown";
+ }
+
+ /**
+ * Encode the current transport into a URI that can be passed via intent calls.
+ * @return URI to host
+ */
+ public static Uri getUri(String input) {
+ return null;
+ }
+
+ /**
+ * Causes transport to connect to the target host. After connecting but before a
+ * session is started, must call back to {@link TerminalBridge#onConnected()}.
+ * After that call a session may be opened.
+ */
+ public abstract void connect();
+
+ /**
+ * Reads from the transport. Transport must support reading into a the byte array
+ * <code>buffer</code> at the start of <code>offset</code> and a maximum of
+ * <code>length</code> bytes. If the remote host disconnects, throw an
+ * {@link IOException}.
+ * @param buffer byte buffer to store read bytes into
+ * @param offset where to start writing in the buffer
+ * @param length maximum number of bytes to read
+ * @return number of bytes read
+ * @throws IOException when remote host disconnects
+ */
+ public abstract int read(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Writes to the transport. If the host is not yet connected, simply return without
+ * doing anything. An {@link IOException} should be thrown if there is an error after
+ * connection.
+ * @param buffer bytes to write to transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(byte[] buffer) throws IOException;
+
+ /**
+ * Writes to the transport. See {@link #write(byte[])} for behavior details.
+ * @param c character to write to the transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(int c) throws IOException;
+
+ /**
+ * Flushes the write commands to the transport.
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void flush() throws IOException;
+
+ /**
+ * Closes the connection to the terminal. Note that the resulting failure to read
+ * should call {@link TerminalBridge#dispatchDisconnect(boolean)}.
+ */
+ public abstract void close();
+
+ /**
+ * Tells the transport what dimensions the display is currently
+ * @param columns columns of text
+ * @param rows rows of text
+ * @param width width in pixels
+ * @param height height in pixels
+ */
+ public abstract void setDimensions(int columns, int rows, int width, int height);
+
+ public void setOptions(Map<String,String> options) {
+ // do nothing
+ }
+
+ public Map<String,String> getOptions() {
+ return null;
+ }
+
+ public void setCompression(boolean compression) {
+ // do nothing
+ }
+
+ public void setUseAuthAgent(String useAuthAgent) {
+ // do nothing
+ }
+
+ public void setEmulation(String emulation) {
+ this.emulation = emulation;
+ }
+
+ public String getEmulation() {
+ return emulation;
+ }
+
+ public void setHost(HostBean host) {
+ this.host = host;
+ }
+
+ public void setBridge(TerminalBridge bridge) {
+ this.bridge = bridge;
+ }
+
+ public void setManager(TerminalManager manager) {
+ this.manager = manager;
+ }
+
+ /**
+ * Whether or not this transport type can forward ports.
+ * @return true on ability to forward ports
+ */
+ public boolean canForwardPorts() {
+ return false;
+ }
+
+ /**
+ * 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 false;
+ }
+
+ /**
+ * Enables a port forward member. After calling this method, the port forward should
+ * be operational iff it could be enabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward setup
+ */
+ public boolean enablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Disables a port forward member. After calling this method, the port forward should
+ * be non-functioning iff it could be disabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward tear-down
+ */
+ public boolean disablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Removes the {@link PortForwardBean} from the available port forwards.
+ * @param portForward the port forward bean to remove
+ * @return true on successful removal
+ */
+ public boolean removePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Gets a list of the {@link PortForwardBean} currently used by this transport.
+ * @return the list of port forwards
+ */
+ public List<PortForwardBean> getPortForwards() {
+ return null;
+ }
+
+ public abstract boolean isConnected();
+ public abstract boolean isSessionOpen();
+
+ /**
+ * @return int default port for protocol
+ */
+ public abstract int getDefaultPort();
+
+ /**
+ * @param username
+ * @param hostname
+ * @param port
+ * @return
+ */
+ public abstract String getDefaultNickname(String username, String hostname, int port);
+
+ /**
+ * @param uri
+ * @param selectionKeys
+ * @param selectionValues
+ */
+ public abstract void getSelectionArgs(Uri uri, Map<String, String> selection);
+
+ /**
+ * @param uri
+ * @return
+ */
+ public abstract HostBean createHost(Uri uri);
+
+ /**
+ * @param context context containing the correct resources
+ * @return string that hints at the format for connection
+ */
+ public static String getFormatHint(Context context) {
+ return "???";
+ }
+
+ /**
+ * @return
+ */
+ public abstract boolean usesNetwork();
+}
diff --git a/app/src/main/java/org/connectbot/transport/Local.java b/app/src/main/java/org/connectbot/transport/Local.java
new file mode 100644
index 0000000..5ace1b0
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/Local.java
@@ -0,0 +1,219 @@
+/*
+ * 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.transport;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.google.ase.Exec;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Local extends AbsTransport {
+ private static final String TAG = "ConnectBot.Local";
+ private static final String PROTOCOL = "local";
+
+ private static final String DEFAULT_URI = "local:#Local";
+
+ private FileDescriptor shellFd;
+
+ private FileInputStream is;
+ private FileOutputStream os;
+
+ /**
+ *
+ */
+ public Local() {
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Local(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void close() {
+ try {
+ if (os != null) {
+ os.close();
+ os = null;
+ }
+ if (is != null) {
+ is.close();
+ is = null;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't close shell", e);
+ }
+ }
+
+ @Override
+ public void connect() {
+ int[] pids = new int[1];
+
+ try {
+ shellFd = Exec.createSubprocess("/system/bin/sh", "-", null, pids);
+ } catch (Exception e) {
+ bridge.outputLine(manager.res.getString(R.string.local_shell_unavailable));
+ Log.e(TAG, "Cannot start local shell", e);
+ return;
+ }
+
+ final int shellPid = pids[0];
+ Runnable exitWatcher = new Runnable() {
+ public void run() {
+ Exec.waitFor(shellPid);
+
+ bridge.dispatchDisconnect(false);
+ }
+ };
+
+ Thread exitWatcherThread = new Thread(exitWatcher);
+ exitWatcherThread.setName("LocalExitWatcher");
+ exitWatcherThread.setDaemon(true);
+ exitWatcherThread.start();
+
+ is = new FileInputStream(shellFd);
+ os = new FileOutputStream(shellFd);
+
+ bridge.onConnected();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ return DEFAULT_URI;
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return 0;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ if (is == null) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("session closed");
+ }
+ return is.read(buffer, start, len);
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ Exec.setPtyWindowSize(shellFd, rows, columns, width, height);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't resize pty", e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (os != null)
+ os.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (os != null)
+ os.write(c);
+ }
+
+ public static Uri getUri(String input) {
+ Uri uri = Uri.parse(DEFAULT_URI);
+
+ if (input != null && input.length() > 0) {
+ uri = uri.buildUpon().fragment(input).build();
+ }
+
+ return uri;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ }
+
+ public static String getFormatHint(Context context) {
+ return context.getString(R.string.hostpref_nickname_title);
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/SSH.java b/app/src/main/java/org/connectbot/transport/SSH.java
new file mode 100644
index 0000000..6ef9745
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/SSH.java
@@ -0,0 +1,958 @@
+/*
+ * 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.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.service.TerminalManager.KeyHolder;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.trilead.ssh2.AuthAgentCallback;
+import com.trilead.ssh2.ChannelCondition;
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.ConnectionMonitor;
+import com.trilead.ssh2.DynamicPortForwarder;
+import com.trilead.ssh2.InteractiveCallback;
+import com.trilead.ssh2.KnownHosts;
+import com.trilead.ssh2.LocalPortForwarder;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.Session;
+import com.trilead.ssh2.crypto.PEMDecoder;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback {
+ public SSH() {
+ super();
+ }
+
+ /**
+ * @param bridge
+ * @param db
+ */
+ public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ private static final String PROTOCOL = "ssh";
+ private static final String TAG = "ConnectBot.SSH";
+ private static final int DEFAULT_PORT = 22;
+
+ private static final String AUTH_PUBLICKEY = "publickey",
+ AUTH_PASSWORD = "password",
+ AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
+
+ private final static int AUTH_TRIES = 20;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^(.+)@([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ private boolean compression = false;
+ private volatile boolean authenticated = false;
+ private volatile boolean connected = false;
+ private volatile boolean sessionOpen = false;
+
+ private boolean pubkeysExhausted = false;
+ private boolean interactiveCanContinue = true;
+
+ private Connection connection;
+ private Session session;
+ private ConnectionInfo connectionInfo;
+
+ private OutputStream stdin;
+ private InputStream stdout;
+ private InputStream stderr;
+
+ private static final int conditions = ChannelCondition.STDOUT_DATA
+ | ChannelCondition.STDERR_DATA
+ | ChannelCondition.CLOSED
+ | ChannelCondition.EOF;
+
+ private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ private int columns;
+ private int rows;
+
+ private int width;
+ private int height;
+
+ private String useAuthAgent = HostDatabase.AUTHAGENT_NO;
+ private String agentLockPassphrase;
+
+ public class HostKeyVerifier implements ServerHostKeyVerifier {
+ public boolean verifyServerHostKey(String hostname, int port,
+ String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
+
+ // read in all known hosts from hostdb
+ KnownHosts hosts = manager.hostdb.getKnownHosts();
+ Boolean result;
+
+ String matchName = String.format(Locale.US, "%s:%d", hostname, port);
+
+ String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
+
+ String algorithmName;
+ if ("ssh-rsa".equals(serverHostKeyAlgorithm))
+ algorithmName = "RSA";
+ else if ("ssh-dss".equals(serverHostKeyAlgorithm))
+ algorithmName = "DSA";
+ else if (serverHostKeyAlgorithm.startsWith("ecdsa-"))
+ algorithmName = "EC";
+ else
+ algorithmName = serverHostKeyAlgorithm;
+
+ switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
+ case KnownHosts.HOSTKEY_IS_OK:
+ bridge.outputLine(manager.res.getString(R.string.terminal_sucess, algorithmName, fingerprint));
+ return true;
+
+ case KnownHosts.HOSTKEY_IS_NEW:
+ // prompt user
+ bridge.outputLine(manager.res.getString(R.string.host_authenticity_warning, hostname));
+ bridge.outputLine(manager.res.getString(R.string.host_fingerprint, algorithmName, fingerprint));
+
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ case KnownHosts.HOSTKEY_HAS_CHANGED:
+ String header = String.format("@ %s @",
+ manager.res.getString(R.string.host_verification_failure_warning_header));
+
+ char[] atsigns = new char[header.length()];
+ Arrays.fill(atsigns, '@');
+ String border = new String(atsigns);
+
+ bridge.outputLine(border);
+ bridge.outputLine(manager.res.getString(R.string.host_verification_failure_warning));
+ bridge.outputLine(border);
+
+ bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint),
+ algorithmName, fingerprint));
+
+ // Users have no way to delete keys, so we'll prompt them for now.
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ default:
+ return false;
+ }
+ }
+
+ }
+
+ private void authenticate() {
+ try {
+ if (connection.authenticateWithNone(host.getUsername())) {
+ finishConnection();
+ return;
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Host does not support 'none' authentication.");
+ }
+
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth));
+
+ try {
+ long pubkeyId = host.getPubkeyId();
+
+ if (!pubkeysExhausted &&
+ pubkeyId != HostDatabase.PUBKEYID_NEVER &&
+ connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) {
+
+ // if explicit pubkey defined for this host, then prompt for password as needed
+ // otherwise just try all in-memory keys held in terminalmanager
+
+ if (pubkeyId == HostDatabase.PUBKEYID_ANY) {
+ // try each of the in-memory keys
+ bridge.outputLine(manager.res
+ .getString(R.string.terminal_auth_pubkey_any));
+ for (Entry<String, KeyHolder> entry : manager.loadedKeypairs.entrySet()) {
+ if (entry.getValue().bean.isConfirmUse()
+ && !promptForPubkeyUse(entry.getKey()))
+ continue;
+
+ if (this.tryPublicKey(host.getUsername(), entry.getKey(),
+ entry.getValue().pair)) {
+ finishConnection();
+ break;
+ }
+ }
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_specific));
+ // use a specific key for this host, as requested
+ PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId);
+
+ if (pubkey == null)
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid));
+ else
+ if (tryPublicKey(pubkey))
+ finishConnection();
+ }
+
+ pubkeysExhausted = true;
+ } else if (interactiveCanContinue &&
+ connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
+ // this auth method will talk with us using InteractiveCallback interface
+ // it blocks until authentication finishes
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki));
+ interactiveCanContinue = false;
+ if(connection.authenticateWithKeyboardInteractive(host.getUsername(), this)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki_fail));
+ }
+ } else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass));
+ String password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.prompt_password));
+ if (password != null
+ && connection.authenticateWithPassword(host.getUsername(), password)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass_fail));
+ }
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_fail));
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Connection went away while we were trying to authenticate", e);
+ return;
+ } catch(Exception e) {
+ Log.e(TAG, "Problem during handleAuthentication()", e);
+ }
+ }
+
+ /**
+ * Attempt connection with database row pointed to by cursor.
+ * @param cursor
+ * @return true for successful authentication
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ * @throws IOException
+ */
+ private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ KeyPair pair = null;
+
+ if(manager.isKeyLoaded(pubkey.getNickname())) {
+ // load this key from memory if its already there
+ Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname()));
+
+ if (pubkey.isConfirmUse()) {
+ if (!promptForPubkeyUse(pubkey.getNickname()))
+ return false;
+ }
+
+ pair = manager.getKey(pubkey.getNickname());
+ } else {
+ // otherwise load key from database and prompt for password as needed
+ String password = null;
+ if (pubkey.isEncrypted()) {
+ password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.prompt_pubkey_password, pubkey.getNickname()));
+
+ // Something must have interrupted the prompt.
+ if (password == null)
+ return false;
+ }
+
+ if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) {
+ // load specific key using pem format
+ pair = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password);
+ } else {
+ // load using internal generated format
+ PrivateKey privKey;
+ try {
+ privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(),
+ pubkey.getType(), password);
+ } catch (Exception e) {
+ String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname());
+ Log.e(TAG, message, e);
+ bridge.outputLine(message);
+ return false;
+ }
+
+ PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
+
+ // convert key to trilead format
+ pair = new KeyPair(pubKey, privKey);
+ Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
+ }
+
+ Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
+
+ // save this key in memory
+ manager.addKey(pubkey, pair);
+ }
+
+ return tryPublicKey(host.getUsername(), pubkey.getNickname(), pair);
+ }
+
+ private boolean tryPublicKey(String username, String keyNickname, KeyPair pair) throws IOException {
+ //bridge.outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
+ boolean success = connection.authenticateWithPublicKey(username, pair);
+ if(!success)
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_fail, keyNickname));
+ return success;
+ }
+
+ /**
+ * Internal method to request actual PTY terminal once we've finished
+ * authentication. If called before authenticated, it will just fail.
+ */
+ private void finishConnection() {
+ authenticated = true;
+
+ for (PortForwardBean portForward : portForwards) {
+ try {
+ enablePortForward(portForward);
+ bridge.outputLine(manager.res.getString(R.string.terminal_enable_portfoward, portForward.getDescription()));
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting up port forward during connect", e);
+ }
+ }
+
+ if (!host.getWantSession()) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_no_session));
+ bridge.onConnected();
+ return;
+ }
+
+ try {
+ session = connection.openSession();
+
+ if (!useAuthAgent.equals(HostDatabase.AUTHAGENT_NO))
+ session.requestAuthAgentForwarding(this);
+
+ session.requestPTY(getEmulation(), columns, rows, width, height, null);
+ session.startShell();
+
+ stdin = session.getStdin();
+ stdout = session.getStdout();
+ stderr = session.getStderr();
+
+ sessionOpen = true;
+
+ bridge.onConnected();
+ } catch (IOException e1) {
+ Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1);
+ }
+
+ }
+
+ @Override
+ public void connect() {
+ connection = new Connection(host.getHostname(), host.getPort());
+ connection.addConnectionMonitor(this);
+
+ try {
+ connection.setCompression(compression);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not enable compression!", e);
+ }
+
+ try {
+ /* Uncomment when debugging SSH protocol:
+ DebugLogger logger = new DebugLogger() {
+
+ public void log(int level, String className, String message) {
+ Log.d("SSH", message);
+ }
+
+ };
+ Logger.enabled = true;
+ Logger.logger = logger;
+ */
+ connectionInfo = connection.connect(new HostKeyVerifier());
+ connected = true;
+
+ if (connectionInfo.clientToServerCryptoAlgorithm
+ .equals(connectionInfo.serverToClientCryptoAlgorithm)
+ && connectionInfo.clientToServerMACAlgorithm
+ .equals(connectionInfo.serverToClientMACAlgorithm)) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_using_algorithm,
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+ } else {
+ bridge.outputLine(manager.res.getString(
+ R.string.terminal_using_c2s_algorithm,
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+
+ bridge.outputLine(manager.res.getString(
+ R.string.terminal_using_s2c_algorithm,
+ connectionInfo.serverToClientCryptoAlgorithm,
+ connectionInfo.serverToClientMACAlgorithm));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+
+ // Display the reason in the text.
+ bridge.outputLine(e.getCause().getMessage());
+
+ onDisconnect();
+ return;
+ }
+
+ try {
+ // enter a loop to keep trying until authentication
+ int tries = 0;
+ while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) {
+ authenticate();
+
+ // sleep to make sure we dont kill system
+ Thread.sleep(1000);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ connected = false;
+
+ if (session != null) {
+ session.close();
+ session = null;
+ }
+
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ }
+
+ private void onDisconnect() {
+ close();
+
+ bridge.dispatchDisconnect(false);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (stdin != null)
+ stdin.flush();
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ int bytesRead = 0;
+
+ if (session == null)
+ return 0;
+
+ int newConditions = session.waitForCondition(conditions, 0);
+
+ if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) {
+ bytesRead = stdout.read(buffer, start, len);
+ }
+
+ if ((newConditions & ChannelCondition.STDERR_DATA) != 0) {
+ byte discard[] = new byte[256];
+ while (stderr.available() > 0) {
+ stderr.read(discard);
+ }
+ }
+
+ if ((newConditions & ChannelCondition.EOF) != 0) {
+ onDisconnect();
+ throw new IOException("Remote end closed connection");
+ }
+
+ return bytesRead;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (stdin != null)
+ stdin.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (stdin != null)
+ stdin.write(c);
+ }
+
+ @Override
+ public Map<String, String> getOptions() {
+ Map<String, String> options = new HashMap<String, String>();
+
+ options.put("compression", Boolean.toString(compression));
+
+ return options;
+ }
+
+ @Override
+ public void setOptions(Map<String, String> options) {
+ if (options.containsKey("compression"))
+ compression = Boolean.parseBoolean(options.get("compression"));
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return sessionOpen;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void connectionLost(Throwable reason) {
+ onDisconnect();
+ }
+
+ @Override
+ public boolean canForwardPorts() {
+ return true;
+ }
+
+ @Override
+ public List<PortForwardBean> getPortForwards() {
+ return portForwards;
+ }
+
+ @Override
+ public boolean addPortForward(PortForwardBean portForward) {
+ return portForwards.add(portForward);
+ }
+
+ @Override
+ public boolean removePortForward(PortForwardBean portForward) {
+ // Make sure we don't have a phantom forwarder.
+ disablePortForward(portForward);
+
+ return portForwards.remove(portForward);
+ }
+
+ @Override
+ public boolean enablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to enable port forward not in list");
+ return false;
+ }
+
+ if (!authenticated)
+ return false;
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ try {
+ lpf = connection.createLocalPortForwarder(
+ new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()),
+ portForward.getDestAddr(), portForward.getDestPort());
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create local port forward", e);
+ return false;
+ }
+
+ if (lpf == null) {
+ Log.e(TAG, "returned LocalPortForwarder object is null");
+ return false;
+ }
+
+ portForward.setIdentifier(lpf);
+ portForward.setEnabled(true);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ try {
+ connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort());
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create remote port forward", e);
+ return false;
+ }
+
+ portForward.setEnabled(true);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+
+ try {
+ dpf = connection.createDynamicPortForwarder(
+ new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()));
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create dynamic port forward", e);
+ return false;
+ }
+
+ portForward.setIdentifier(dpf);
+ portForward.setEnabled(true);
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public boolean disablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to disable port forward not in list");
+ return false;
+ }
+
+ if (!authenticated)
+ return false;
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ lpf = (LocalPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || lpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ lpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ portForward.setEnabled(false);
+
+ try {
+ connection.cancelRemotePortForwarding(portForward.getSourcePort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+ dpf = (DynamicPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || dpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ dpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ this.columns = columns;
+ this.rows = rows;
+
+ if (sessionOpen) {
+ try {
+ session.resizePTY(columns, rows, width, height);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't send resize PTY packet", e);
+ }
+ }
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format(Locale.US, "%s@%s", username, hostname);
+ } else {
+ return String.format(Locale.US, "%s@%s:%d", username, hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(Uri.encode(matcher.group(1)))
+ .append('@')
+ .append(matcher.group(2));
+
+ String portString = matcher.group(4);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':')
+ .append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ /**
+ * Handle challenges from keyboard-interactive authentication mode.
+ */
+ public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
+ interactiveCanContinue = true;
+ String[] responses = new String[numPrompts];
+ for(int i = 0; i < numPrompts; i++) {
+ // request response from user for each prompt
+ responses[i] = bridge.promptHelper.requestStringPrompt(instruction, prompt[i]);
+ }
+ return responses;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ host.setHostname(uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+
+ host.setUsername(uri.getUserInfo());
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ selection.put(HostDatabase.FIELD_HOST_USERNAME, uri.getUserInfo());
+ }
+
+ @Override
+ public void setCompression(boolean compression) {
+ this.compression = compression;
+ }
+
+ public static String getFormatHint(Context context) {
+ return String.format("%s@%s:%s",
+ context.getString(R.string.format_username),
+ context.getString(R.string.format_hostname),
+ context.getString(R.string.format_port));
+ }
+
+ @Override
+ public void setUseAuthAgent(String useAuthAgent) {
+ this.useAuthAgent = useAuthAgent;
+ }
+
+ public Map<String,byte[]> retrieveIdentities() {
+ Map<String,byte[]> pubKeys = new HashMap<String,byte[]>(manager.loadedKeypairs.size());
+
+ for (Entry<String,KeyHolder> entry : manager.loadedKeypairs.entrySet()) {
+ KeyPair pair = entry.getValue().pair;
+
+ try {
+ PrivateKey privKey = pair.getPrivate();
+ if (privKey instanceof RSAPrivateKey) {
+ RSAPublicKey pubkey = (RSAPublicKey) pair.getPublic();
+ pubKeys.put(entry.getKey(), RSASHA1Verify.encodeSSHRSAPublicKey(pubkey));
+ } else if (privKey instanceof DSAPrivateKey) {
+ DSAPublicKey pubkey = (DSAPublicKey) pair.getPublic();
+ pubKeys.put(entry.getKey(), DSASHA1Verify.encodeSSHDSAPublicKey(pubkey));
+ } else
+ continue;
+ } catch (IOException e) {
+ continue;
+ }
+ }
+
+ return pubKeys;
+ }
+
+ public KeyPair getKeyPair(byte[] publicKey) {
+ String nickname = manager.getKeyNickname(publicKey);
+
+ if (nickname == null)
+ return null;
+
+ if (useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) {
+ Log.e(TAG, "");
+ return null;
+ } else if (useAuthAgent.equals(HostDatabase.AUTHAGENT_CONFIRM) ||
+ manager.loadedKeypairs.get(nickname).bean.isConfirmUse()) {
+ if (!promptForPubkeyUse(nickname))
+ return null;
+ }
+ return manager.getKey(nickname);
+ }
+
+ private boolean promptForPubkeyUse(String nickname) {
+ Boolean result = bridge.promptHelper.requestBooleanPrompt(null,
+ manager.res.getString(R.string.prompt_allow_agent_to_use_key,
+ nickname));
+ return result;
+ }
+
+ public boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime) {
+ PubkeyBean pubkey = new PubkeyBean();
+// pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED);
+ pubkey.setNickname(comment);
+ pubkey.setConfirmUse(confirmUse);
+ pubkey.setLifetime(lifetime);
+ manager.addKey(pubkey, pair);
+ return true;
+ }
+
+ public boolean removeAllIdentities() {
+ manager.loadedKeypairs.clear();
+ return true;
+ }
+
+ public boolean removeIdentity(byte[] publicKey) {
+ return manager.removeKey(publicKey);
+ }
+
+ public boolean isAgentLocked() {
+ return agentLockPassphrase != null;
+ }
+
+ public boolean requestAgentUnlock(String unlockPassphrase) {
+ if (agentLockPassphrase == null)
+ return false;
+
+ if (agentLockPassphrase.equals(unlockPassphrase))
+ agentLockPassphrase = null;
+
+ return agentLockPassphrase == null;
+ }
+
+ public boolean setAgentLock(String lockPassphrase) {
+ if (agentLockPassphrase != null)
+ return false;
+
+ agentLockPassphrase = lockPassphrase;
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/Telnet.java b/app/src/main/java/org/connectbot/transport/Telnet.java
new file mode 100644
index 0000000..5fde2f6
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/Telnet.java
@@ -0,0 +1,330 @@
+/*
+ * 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.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+import de.mud.telnet.TelnetProtocolHandler;
+
+/**
+ * Telnet transport implementation.<br/>
+ * Original idea from the JTA telnet package (de.mud.telnet)
+ *
+ * @author Kenny Root
+ *
+ */
+public class Telnet extends AbsTransport {
+ private static final String TAG = "ConnectBot.Telnet";
+ private static final String PROTOCOL = "telnet";
+
+ private static final int DEFAULT_PORT = 23;
+
+ private TelnetProtocolHandler handler;
+ private Socket socket;
+
+ private InputStream is;
+ private OutputStream os;
+ private int width;
+ private int height;
+
+ private boolean connected = false;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ public Telnet() {
+ handler = new TelnetProtocolHandler() {
+ /** get the current terminal type */
+ @Override
+ public String getTerminalType() {
+ return getEmulation();
+ }
+
+ /** get the current window size */
+ @Override
+ public int[] getWindowSize() {
+ return new int[] { width, height };
+ }
+
+ /** notify about local echo */
+ @Override
+ public void setLocalEcho(boolean echo) {
+ /* EMPTY */
+ }
+
+ /** write data to our back end */
+ @Override
+ public void write(byte[] b) throws IOException {
+ if (os != null)
+ os.write(b);
+ }
+
+ /** sent on IAC EOR (prompt terminator for remote access systems). */
+ @Override
+ public void notifyEndOfRecord() {
+ }
+
+ @Override
+ protected String getCharsetName() {
+ Charset charset = bridge.getCharset();
+ if (charset != null)
+ return charset.name();
+ else
+ return "";
+ }
+ };
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Telnet(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void connect() {
+ try {
+ socket = new Socket(host.getHostname(), host.getPort());
+
+ connected = true;
+
+ is = socket.getInputStream();
+ os = socket.getOutputStream();
+
+ bridge.onConnected();
+ } catch (UnknownHostException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ } catch (IOException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ connected = false;
+ if (socket != null)
+ try {
+ socket.close();
+ socket = null;
+ } catch (IOException e) {
+ Log.d(TAG, "Error closing telnet socket.", e);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return connected;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ /* process all already read bytes */
+ int n = 0;
+
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return n;
+ } while (n == 0);
+
+ while (n <= 0) {
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return n;
+ } while (n == 0);
+ n = is.read(buffer, start, len);
+ if (n < 0) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("Remote end closed connection.");
+ }
+
+ handler.inputfeed(buffer, start, n);
+ n = handler.negotiate(buffer, start);
+ }
+ return n;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ try {
+ if (os != null)
+ os.write(buffer);
+ } catch (SocketException e) {
+ bridge.dispatchDisconnect(false);
+ }
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ try {
+ if (os != null)
+ os.write(c);
+ } catch (SocketException e) {
+ bridge.dispatchDisconnect(false);
+ }
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ handler.setWindowSize(columns, rows);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't resize remote terminal", e);
+ }
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format("%s", hostname);
+ } else {
+ return String.format("%s:%d", hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(matcher.group(1));
+
+ String portString = matcher.group(3);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':');
+ sb.append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ host.setHostname(uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ }
+
+ public static String getFormatHint(Context context) {
+ return String.format("%s:%s",
+ context.getString(R.string.format_hostname),
+ context.getString(R.string.format_port));
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/TransportFactory.java b/app/src/main/java/org/connectbot/transport/TransportFactory.java
new file mode 100644
index 0000000..72e5e08
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/TransportFactory.java
@@ -0,0 +1,132 @@
+/*
+ * 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.transport;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class TransportFactory {
+ private static final String TAG = "ConnectBot.TransportFactory";
+
+ private static String[] transportNames = {
+ SSH.getProtocolName(),
+ Telnet.getProtocolName(),
+ Local.getProtocolName(),
+ };
+
+ /**
+ * @param protocol
+ * @return
+ */
+ public static AbsTransport getTransport(String protocol) {
+ if (SSH.getProtocolName().equals(protocol)) {
+ return new SSH();
+ } else if (Telnet.getProtocolName().equals(protocol)) {
+ return new Telnet();
+ } else if (Local.getProtocolName().equals(protocol)) {
+ return new Local();
+ } else {
+ return null;
+ }
+ }
+
+ public static Uri getUri(String scheme, String input) {
+ Log.d("TransportFactory", String.format(
+ "Attempting to discover URI for scheme=%s on input=%s", scheme,
+ input));
+ if (SSH.getProtocolName().equals(scheme))
+ return SSH.getUri(input);
+ else if (Telnet.getProtocolName().equals(scheme))
+ return Telnet.getUri(input);
+ else if (Local.getProtocolName().equals(scheme)) {
+ Log.d("TransportFactory", "Got to the local parsing area");
+ return Local.getUri(input);
+ } else
+ return null;
+ }
+
+ public static String[] getTransportNames() {
+ return transportNames;
+ }
+
+ public static boolean isSameTransportType(AbsTransport a, AbsTransport b) {
+ if (a == null || b == null)
+ return false;
+
+ return a.getClass().equals(b.getClass());
+ }
+
+ public static boolean canForwardPorts(String protocol) {
+ // TODO uh, make this have less knowledge about its children
+ if (SSH.getProtocolName().equals(protocol)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param protocol text name of protocol
+ * @param context
+ * @return expanded format hint
+ */
+ public static String getFormatHint(String protocol, Context context) {
+ if (SSH.getProtocolName().equals(protocol)) {
+ return SSH.getFormatHint(context);
+ } else if (Telnet.getProtocolName().equals(protocol)) {
+ return Telnet.getFormatHint(context);
+ } else if (Local.getProtocolName().equals(protocol)) {
+ return Local.getFormatHint(context);
+ } else {
+ return AbsTransport.getFormatHint(context);
+ }
+ }
+
+ /**
+ * @param hostdb Handle to HostDatabase
+ * @param uri URI to target server
+ * @param host HostBean in which to put the results
+ * @return true when host was found
+ */
+ public static HostBean findHost(HostDatabase hostdb, Uri uri) {
+ AbsTransport transport = getTransport(uri.getScheme());
+
+ Map<String, String> selection = new HashMap<String, String>();
+
+ transport.getSelectionArgs(uri, selection);
+ if (selection.size() == 0) {
+ Log.e(TAG, String.format("Transport %s failed to do something useful with URI=%s",
+ uri.getScheme(), uri.toString()));
+ throw new IllegalStateException("Failed to get needed selection arguments");
+ }
+
+ return hostdb.findHost(selection);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/Colors.java b/app/src/main/java/org/connectbot/util/Colors.java
new file mode 100644
index 0000000..ff88d68
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/Colors.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Colors {
+ public final static Integer[] defaults = new Integer[] {
+ 0xff000000, // black
+ 0xffcc0000, // red
+ 0xff00cc00, // green
+ 0xffcccc00, // brown
+ 0xff0000cc, // blue
+ 0xffcc00cc, // purple
+ 0xff00cccc, // cyan
+ 0xffcccccc, // light grey
+ 0xff444444, // dark grey
+ 0xffff4444, // light red
+ 0xff44ff44, // light green
+ 0xffffff44, // yellow
+ 0xff4444ff, // light blue
+ 0xffff44ff, // light purple
+ 0xff44ffff, // light cyan
+ 0xffffffff, // white
+ 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7,
+ 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf,
+ 0xff005fd7, 0xff005fff, 0xff008700, 0xff00875f, 0xff008787,
+ 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f,
+ 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff, 0xff00d700,
+ 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff,
+ 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7,
+ 0xff00ffff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af,
+ 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87,
+ 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f8700, 0xff5f875f,
+ 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00,
+ 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
+ 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7,
+ 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf,
+ 0xff5fffd7, 0xff5fffff, 0xff870000, 0xff87005f, 0xff870087,
+ 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f,
+ 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff, 0xff878700,
+ 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff,
+ 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7,
+ 0xff87afff, 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af,
+ 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87,
+ 0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xffaf0000, 0xffaf005f,
+ 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00,
+ 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
+ 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7,
+ 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf,
+ 0xffafafd7, 0xffafafff, 0xffafd700, 0xffafd75f, 0xffafd787,
+ 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f,
+ 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffd70000,
+ 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff,
+ 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7,
+ 0xffd75fff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af,
+ 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87,
+ 0xffd7afaf, 0xffd7afd7, 0xffd7afff, 0xffd7d700, 0xffd7d75f,
+ 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00,
+ 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
+ 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7,
+ 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf,
+ 0xffff5fd7, 0xffff5fff, 0xffff8700, 0xffff875f, 0xffff8787,
+ 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f,
+ 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff, 0xffffd700,
+ 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff,
+ 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7,
+ 0xffffffff, 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626,
+ 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858,
+ 0xff626262, 0xff6c6c6c, 0xff767676, 0xff808080, 0xff8a8a8a,
+ 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc,
+ 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
+ };
+}
diff --git a/app/src/main/java/org/connectbot/util/EastAsianWidth.java b/app/src/main/java/org/connectbot/util/EastAsianWidth.java
new file mode 100644
index 0000000..0e274b5
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EastAsianWidth.java
@@ -0,0 +1,75 @@
+/*
+ * 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.util;
+
+import android.graphics.Paint;
+import android.text.AndroidCharacter;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class EastAsianWidth {
+ public static EastAsianWidth getInstance() {
+ if (PreferenceConstants.PRE_FROYO)
+ return PreFroyo.Holder.sInstance;
+ else
+ return FroyoAndBeyond.Holder.sInstance;
+ }
+
+ /**
+ * @param charArray
+ * @param i
+ * @param position
+ * @param wideAttribute
+ */
+ public abstract void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth);
+
+ private static class PreFroyo extends EastAsianWidth {
+ private static final int BUFFER_SIZE = 4096;
+ private float[] mWidths = new float[BUFFER_SIZE];
+
+ private static class Holder {
+ private static final PreFroyo sInstance = new PreFroyo();
+ }
+
+ @Override
+ public void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth) {
+ paint.getTextWidths(charArray, start, end, mWidths);
+ final int N = end - start;
+ for (int i = 0; i < N; i++)
+ wideAttribute[i] = (byte) (((int)mWidths[i] != charWidth) ?
+ AndroidCharacter.EAST_ASIAN_WIDTH_WIDE :
+ AndroidCharacter.EAST_ASIAN_WIDTH_NARROW);
+ }
+ }
+
+ private static class FroyoAndBeyond extends EastAsianWidth {
+ private static class Holder {
+ private static final FroyoAndBeyond sInstance = new FroyoAndBeyond();
+ }
+
+ @Override
+ public void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth) {
+ AndroidCharacter.getEastAsianWidths(charArray, start, end - start, wideAttribute);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/Encryptor.java b/app/src/main/java/org/connectbot/util/Encryptor.java
new file mode 100644
index 0000000..9d21454
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/Encryptor.java
@@ -0,0 +1,205 @@
+/*
+ * 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.util;
+
+/**
+ * This class is from:
+ *
+ * Encryptor.java
+ * Copyright 2008 Zach Scrivena
+ * zachscrivena@gmail.com
+ * http://zs.freeshell.org/
+ */
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+/**
+ * Perform AES-128 encryption.
+ */
+public final class Encryptor
+{
+ /** name of the character set to use for converting between characters and bytes */
+ private static final String CHARSET_NAME = "UTF-8";
+
+ /** random number generator algorithm */
+ private static final String RNG_ALGORITHM = "SHA1PRNG";
+
+ /** message digest algorithm (must be sufficiently long to provide the key and initialization vector) */
+ private static final String DIGEST_ALGORITHM = "SHA-256";
+
+ /** key algorithm (must be compatible with CIPHER_ALGORITHM) */
+ private static final String KEY_ALGORITHM = "AES";
+
+ /** cipher algorithm (must be compatible with KEY_ALGORITHM) */
+ private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+
+
+ /**
+ * Private constructor that should never be called.
+ */
+ private Encryptor()
+ {}
+
+
+ /**
+ * Encrypt the specified cleartext using the given password.
+ * With the correct salt, number of iterations, and password, the decrypt() method reverses
+ * the effect of this method.
+ * This method generates and uses a random salt, and the user-specified number of iterations
+ * and password to create a 16-byte secret key and 16-byte initialization vector.
+ * The secret key and initialization vector are then used in the AES-128 cipher to encrypt
+ * the given cleartext.
+ *
+ * @param salt
+ * salt that was used in the encryption (to be populated)
+ * @param iterations
+ * number of iterations to use in salting
+ * @param password
+ * password to be used for encryption
+ * @param cleartext
+ * cleartext to be encrypted
+ * @return
+ * ciphertext
+ * @throws Exception
+ * on any error encountered in encryption
+ */
+ public static byte[] encrypt(
+ final byte[] salt,
+ final int iterations,
+ final String password,
+ final byte[] cleartext)
+ throws Exception
+ {
+ /* generate salt randomly */
+ SecureRandom.getInstance(RNG_ALGORITHM).nextBytes(salt);
+
+ /* compute key and initialization vector */
+ final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+ byte[] pw = password.getBytes(CHARSET_NAME);
+
+ for (int i = 0; i < iterations; i++)
+ {
+ /* add salt */
+ final byte[] salted = new byte[pw.length + salt.length];
+ System.arraycopy(pw, 0, salted, 0, pw.length);
+ System.arraycopy(salt, 0, salted, pw.length, salt.length);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* compute SHA-256 digest */
+ shaDigest.reset();
+ pw = shaDigest.digest(salted);
+ Arrays.fill(salted, (byte) 0x00);
+ }
+
+ /* extract the 16-byte key and initialization vector from the SHA-256 digest */
+ final byte[] key = new byte[16];
+ final byte[] iv = new byte[16];
+ System.arraycopy(pw, 0, key, 0, 16);
+ System.arraycopy(pw, 16, iv, 0, 16);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* perform AES-128 encryption */
+ final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ new SecretKeySpec(key, KEY_ALGORITHM),
+ new IvParameterSpec(iv));
+
+ Arrays.fill(key, (byte) 0x00);
+ Arrays.fill(iv, (byte) 0x00);
+
+ return cipher.doFinal(cleartext);
+ }
+
+
+ /**
+ * Decrypt the specified ciphertext using the given password.
+ * With the correct salt, number of iterations, and password, this method reverses the effect
+ * of the encrypt() method.
+ * This method uses the user-specified salt, number of iterations, and password
+ * to recreate the 16-byte secret key and 16-byte initialization vector.
+ * The secret key and initialization vector are then used in the AES-128 cipher to decrypt
+ * the given ciphertext.
+ *
+ * @param salt
+ * salt to be used in decryption
+ * @param iterations
+ * number of iterations to use in salting
+ * @param password
+ * password to be used for decryption
+ * @param ciphertext
+ * ciphertext to be decrypted
+ * @return
+ * cleartext
+ * @throws Exception
+ * on any error encountered in decryption
+ */
+ public static byte[] decrypt(
+ final byte[] salt,
+ final int iterations,
+ final String password,
+ final byte[] ciphertext)
+ throws Exception
+ {
+ /* compute key and initialization vector */
+ final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+ byte[] pw = password.getBytes(CHARSET_NAME);
+
+ for (int i = 0; i < iterations; i++)
+ {
+ /* add salt */
+ final byte[] salted = new byte[pw.length + salt.length];
+ System.arraycopy(pw, 0, salted, 0, pw.length);
+ System.arraycopy(salt, 0, salted, pw.length, salt.length);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* compute SHA-256 digest */
+ shaDigest.reset();
+ pw = shaDigest.digest(salted);
+ Arrays.fill(salted, (byte) 0x00);
+ }
+
+ /* extract the 16-byte key and initialization vector from the SHA-256 digest */
+ final byte[] key = new byte[16];
+ final byte[] iv = new byte[16];
+ System.arraycopy(pw, 0, key, 0, 16);
+ System.arraycopy(pw, 16, iv, 0, 16);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* perform AES-128 decryption */
+ final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ new SecretKeySpec(key, KEY_ALGORITHM),
+ new IvParameterSpec(iv));
+
+ Arrays.fill(key, (byte) 0x00);
+ Arrays.fill(iv, (byte) 0x00);
+
+ return cipher.doFinal(ciphertext);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/EntropyDialog.java b/app/src/main/java/org/connectbot/util/EntropyDialog.java
new file mode 100644
index 0000000..4498ce2
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EntropyDialog.java
@@ -0,0 +1,50 @@
+/*
+ * 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.util;
+
+import org.connectbot.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.View;
+
+public class EntropyDialog extends Dialog implements OnEntropyGatheredListener {
+
+ public EntropyDialog(Context context) {
+ super(context);
+
+ this.setContentView(R.layout.dia_gatherentropy);
+ this.setTitle(R.string.pubkey_gather_entropy);
+
+ ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this);
+ }
+
+ public EntropyDialog(Context context, View view) {
+ super(context);
+
+ this.setContentView(view);
+ this.setTitle(R.string.pubkey_gather_entropy);
+
+ ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this);
+ }
+
+ public void onEntropyGathered(byte[] entropy) {
+ this.dismiss();
+ }
+
+}
diff --git a/app/src/main/java/org/connectbot/util/EntropyView.java b/app/src/main/java/org/connectbot/util/EntropyView.java
new file mode 100644
index 0000000..c988673
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EntropyView.java
@@ -0,0 +1,169 @@
+/*
+ * 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.util;
+
+import java.util.Vector;
+
+import org.connectbot.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.FontMetrics;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class EntropyView extends View {
+ private static final int SHA1_MAX_BYTES = 20;
+ private static final int MILLIS_BETWEEN_INPUTS = 50;
+
+ private Paint mPaint;
+ private FontMetrics mFontMetrics;
+ private boolean mFlipFlop;
+ private long mLastTime;
+ private Vector<OnEntropyGatheredListener> listeners;
+
+ private byte[] mEntropy;
+ private int mEntropyByteIndex;
+ private int mEntropyBitIndex;
+
+ private int splitText = 0;
+
+ private float lastX = 0.0f, lastY = 0.0f;
+
+ public EntropyView(Context context) {
+ super(context);
+
+ setUpEntropy();
+ }
+
+ public EntropyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setUpEntropy();
+ }
+
+ private void setUpEntropy() {
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setTypeface(Typeface.DEFAULT);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ mPaint.setTextSize(16);
+ mPaint.setColor(Color.WHITE);
+ mFontMetrics = mPaint.getFontMetrics();
+
+ mEntropy = new byte[SHA1_MAX_BYTES];
+ mEntropyByteIndex = 0;
+ mEntropyBitIndex = 0;
+
+ listeners = new Vector<OnEntropyGatheredListener>();
+ }
+
+ public void addOnEntropyGatheredListener(OnEntropyGatheredListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeOnEntropyGatheredListener(OnEntropyGatheredListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ String prompt = String.format(getResources().getString(R.string.pubkey_touch_prompt),
+ (int)(100.0 * (mEntropyByteIndex / 20.0)) + (int)(5.0 * (mEntropyBitIndex / 8.0)));
+ if (splitText > 0 ||
+ mPaint.measureText(prompt) > (getWidth() * 0.8)) {
+ if (splitText == 0)
+ splitText = prompt.indexOf(" ", prompt.length() / 2);
+
+ c.drawText(prompt.substring(0, splitText),
+ getWidth() / 2.0f,
+ getHeight() / 2.0f + (mPaint.ascent() + mPaint.descent()),
+ mPaint);
+ c.drawText(prompt.substring(splitText),
+ getWidth() / 2.0f,
+ getHeight() / 2.0f - (mPaint.ascent() + mPaint.descent()),
+ mPaint);
+ } else {
+ c.drawText(prompt,
+ getWidth() / 2.0f,
+ getHeight() / 2.0f - (mFontMetrics.ascent + mFontMetrics.descent) / 2,
+ mPaint);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mEntropyByteIndex >= SHA1_MAX_BYTES
+ || lastX == event.getX()
+ || lastY == event.getY())
+ return true;
+
+ // Only get entropy every 200 milliseconds to ensure the user has moved around.
+ long now = System.currentTimeMillis();
+ if ((now - mLastTime) < MILLIS_BETWEEN_INPUTS)
+ return true;
+ else
+ mLastTime = now;
+
+ byte input;
+
+ lastX = event.getX();
+ lastY = event.getY();
+
+ // Get the lowest 4 bits of each X, Y input and concat to the entropy-gathering
+ // string.
+ if (mFlipFlop)
+ input = (byte)((((int)lastX & 0x0F) << 4) | ((int)lastY & 0x0F));
+ else
+ input = (byte)((((int)lastY & 0x0F) << 4) | ((int)lastX & 0x0F));
+ mFlipFlop = !mFlipFlop;
+
+ for (int i = 0; i < 4 && mEntropyByteIndex < SHA1_MAX_BYTES; i++) {
+ if ((input & 0x3) == 0x1) {
+ mEntropy[mEntropyByteIndex] <<= 1;
+ mEntropy[mEntropyByteIndex] |= 1;
+ mEntropyBitIndex++;
+ input >>= 2;
+ } else if ((input & 0x3) == 0x2) {
+ mEntropy[mEntropyByteIndex] <<= 1;
+ mEntropyBitIndex++;
+ input >>= 2;
+ }
+
+ if (mEntropyBitIndex >= 8) {
+ mEntropyBitIndex = 0;
+ mEntropyByteIndex++;
+ }
+ }
+
+ // SHA1PRNG only keeps 160 bits of entropy.
+ if (mEntropyByteIndex >= SHA1_MAX_BYTES) {
+ for (OnEntropyGatheredListener listener: listeners) {
+ listener.onEntropyGathered(mEntropy);
+ }
+ }
+
+ invalidate();
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/HelpTopicView.java b/app/src/main/java/org/connectbot/util/HelpTopicView.java
new file mode 100644
index 0000000..0cbc267
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/HelpTopicView.java
@@ -0,0 +1,62 @@
+/*
+ * 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.util;
+
+import org.connectbot.HelpActivity;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpTopicView extends WebView {
+ public HelpTopicView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize();
+ }
+
+ public HelpTopicView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public HelpTopicView(Context context) {
+ super(context);
+ initialize();
+ }
+
+ private void initialize() {
+ WebSettings wSet = getSettings();
+ wSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+ wSet.setUseWideViewPort(false);
+ }
+
+ public HelpTopicView setTopic(String topic) {
+ String path = String.format("file:///android_asset/%s/%s%s",
+ HelpActivity.HELPDIR, topic, HelpActivity.SUFFIX);
+ loadUrl(path);
+
+ computeScroll();
+
+ return this;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/HostDatabase.java b/app/src/main/java/org/connectbot/util/HostDatabase.java
new file mode 100644
index 0000000..2a92bab
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/HostDatabase.java
@@ -0,0 +1,766 @@
+/*
+ * 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.util;
+
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.trilead.ssh2.KnownHosts;
+
+/**
+ * Contains information about various SSH hosts, include public hostkey if known
+ * from previous sessions.
+ *
+ * @author jsharkey
+ */
+public class HostDatabase extends RobustSQLiteOpenHelper {
+
+ public final static String TAG = "ConnectBot.HostDatabase";
+
+ public final static String DB_NAME = "hosts";
+ public final static int DB_VERSION = 22;
+
+ public final static String TABLE_HOSTS = "hosts";
+ public final static String FIELD_HOST_NICKNAME = "nickname";
+ public final static String FIELD_HOST_PROTOCOL = "protocol";
+ public final static String FIELD_HOST_USERNAME = "username";
+ public final static String FIELD_HOST_HOSTNAME = "hostname";
+ public final static String FIELD_HOST_PORT = "port";
+ public final static String FIELD_HOST_HOSTKEYALGO = "hostkeyalgo";
+ public final static String FIELD_HOST_HOSTKEY = "hostkey";
+ public final static String FIELD_HOST_LASTCONNECT = "lastconnect";
+ public final static String FIELD_HOST_COLOR = "color";
+ public final static String FIELD_HOST_USEKEYS = "usekeys";
+ public final static String FIELD_HOST_USEAUTHAGENT = "useauthagent";
+ public final static String FIELD_HOST_POSTLOGIN = "postlogin";
+ public final static String FIELD_HOST_PUBKEYID = "pubkeyid";
+ public final static String FIELD_HOST_WANTSESSION = "wantsession";
+ public final static String FIELD_HOST_DELKEY = "delkey";
+ public final static String FIELD_HOST_FONTSIZE = "fontsize";
+ public final static String FIELD_HOST_COMPRESSION = "compression";
+ public final static String FIELD_HOST_ENCODING = "encoding";
+ public final static String FIELD_HOST_STAYCONNECTED = "stayconnected";
+
+ public final static String TABLE_PORTFORWARDS = "portforwards";
+ public final static String FIELD_PORTFORWARD_HOSTID = "hostid";
+ public final static String FIELD_PORTFORWARD_NICKNAME = "nickname";
+ public final static String FIELD_PORTFORWARD_TYPE = "type";
+ public final static String FIELD_PORTFORWARD_SOURCEPORT = "sourceport";
+ public final static String FIELD_PORTFORWARD_DESTADDR = "destaddr";
+ public final static String FIELD_PORTFORWARD_DESTPORT = "destport";
+
+ public final static String TABLE_COLORS = "colors";
+ public final static String FIELD_COLOR_SCHEME = "scheme";
+ public final static String FIELD_COLOR_NUMBER = "number";
+ public final static String FIELD_COLOR_VALUE = "value";
+
+ public final static String TABLE_COLOR_DEFAULTS = "colorDefaults";
+ public final static String FIELD_COLOR_FG = "fg";
+ public final static String FIELD_COLOR_BG = "bg";
+
+ public final static int DEFAULT_FG_COLOR = 7;
+ public final static int DEFAULT_BG_COLOR = 0;
+
+ public final static String COLOR_RED = "red";
+ public final static String COLOR_GREEN = "green";
+ public final static String COLOR_BLUE = "blue";
+ public final static String COLOR_GRAY = "gray";
+
+ public final static String PORTFORWARD_LOCAL = "local";
+ public final static String PORTFORWARD_REMOTE = "remote";
+ public final static String PORTFORWARD_DYNAMIC4 = "dynamic4";
+ public final static String PORTFORWARD_DYNAMIC5 = "dynamic5";
+
+ public final static String DELKEY_DEL = "del";
+ public final static String DELKEY_BACKSPACE = "backspace";
+
+ public final static String AUTHAGENT_NO = "no";
+ public final static String AUTHAGENT_CONFIRM = "confirm";
+ public final static String AUTHAGENT_YES = "yes";
+
+ public final static String ENCODING_DEFAULT = Charset.defaultCharset().name();
+
+ public final static long PUBKEYID_NEVER = -2;
+ public final static long PUBKEYID_ANY = -1;
+
+ public static final int DEFAULT_COLOR_SCHEME = 0;
+
+ // Table creation strings
+ public static final String CREATE_TABLE_COLOR_DEFAULTS =
+ "CREATE TABLE " + TABLE_COLOR_DEFAULTS
+ + " (" + FIELD_COLOR_SCHEME + " INTEGER NOT NULL, "
+ + FIELD_COLOR_FG + " INTEGER NOT NULL DEFAULT " + DEFAULT_FG_COLOR + ", "
+ + FIELD_COLOR_BG + " INTEGER NOT NULL DEFAULT " + DEFAULT_BG_COLOR + ")";
+ public static final String CREATE_TABLE_COLOR_DEFAULTS_INDEX =
+ "CREATE INDEX " + TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLOR_DEFAULTS + " (" + FIELD_COLOR_SCHEME + ");";
+
+ private static final String WHERE_SCHEME_AND_COLOR = FIELD_COLOR_SCHEME + " = ? AND "
+ + FIELD_COLOR_NUMBER + " = ?";
+
+ static {
+ addTableName(TABLE_HOSTS);
+ addTableName(TABLE_PORTFORWARDS);
+ addIndexName(TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index");
+ addTableName(TABLE_COLORS);
+ addIndexName(TABLE_COLORS + FIELD_COLOR_SCHEME + "index");
+ addTableName(TABLE_COLOR_DEFAULTS);
+ addIndexName(TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index");
+ }
+
+ public static final Object[] dbLock = new Object[0];
+
+ public HostDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+
+ getWritableDatabase().close();
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ super.onCreate(db);
+
+ db.execSQL("CREATE TABLE " + TABLE_HOSTS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_HOST_NICKNAME + " TEXT, "
+ + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh', "
+ + FIELD_HOST_USERNAME + " TEXT, "
+ + FIELD_HOST_HOSTNAME + " TEXT, "
+ + FIELD_HOST_PORT + " INTEGER, "
+ + FIELD_HOST_HOSTKEYALGO + " TEXT, "
+ + FIELD_HOST_HOSTKEY + " BLOB, "
+ + FIELD_HOST_LASTCONNECT + " INTEGER, "
+ + FIELD_HOST_COLOR + " TEXT, "
+ + FIELD_HOST_USEKEYS + " TEXT, "
+ + FIELD_HOST_USEAUTHAGENT + " TEXT, "
+ + FIELD_HOST_POSTLOGIN + " TEXT, "
+ + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY + ", "
+ + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "', "
+ + FIELD_HOST_FONTSIZE + " INTEGER, "
+ + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "', "
+ + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', "
+ + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "', "
+ + FIELD_HOST_STAYCONNECTED + " TEXT)");
+
+ db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
+ + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
+ + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
+ + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
+ + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
+ + FIELD_PORTFORWARD_DESTPORT + " TEXT)");
+
+ db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON "
+ + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");");
+
+ db.execSQL("CREATE TABLE " + TABLE_COLORS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_COLOR_NUMBER + " INTEGER, "
+ + FIELD_COLOR_VALUE + " INTEGER, "
+ + FIELD_COLOR_SCHEME + " INTEGER)");
+
+ db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");");
+
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX);
+ }
+
+ @Override
+ public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException {
+ // Versions of the database before the Android Market release will be
+ // shot without warning.
+ if (oldVersion <= 9) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_HOSTS);
+ onCreate(db);
+ return;
+ }
+
+ switch (oldVersion) {
+ case 10:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY);
+ case 11:
+ db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
+ + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
+ + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
+ + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
+ + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
+ + FIELD_PORTFORWARD_DESTPORT + " INTEGER)");
+ case 12:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "'");
+ case 13:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "'");
+ case 14:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "'");
+ case 15:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh'");
+ case 16:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "'");
+ case 17:
+ db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON "
+ + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");");
+
+ // Add colors
+ db.execSQL("CREATE TABLE " + TABLE_COLORS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_COLOR_NUMBER + " INTEGER, "
+ + FIELD_COLOR_VALUE + " INTEGER, "
+ + FIELD_COLOR_SCHEME + " INTEGER)");
+ db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");");
+ case 18:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_USEAUTHAGENT + " TEXT DEFAULT '" + AUTHAGENT_NO + "'");
+ case 19:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_STAYCONNECTED + " TEXT");
+ case 20:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_FONTSIZE + " INTEGER");
+ case 21:
+ db.execSQL("DROP TABLE " + TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX);
+ }
+ }
+
+ /**
+ * Touch a specific host to update its "last connected" field.
+ * @param nickname Nickname field of host to update
+ */
+ public void touchHost(HostBean host) {
+ long now = System.currentTimeMillis() / 1000;
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_LASTCONNECT, now);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ db.update(TABLE_HOSTS, values, "_id = ?", new String[] { String.valueOf(host.getId()) });
+ }
+ }
+
+ /**
+ * Create a new host using the given parameters.
+ */
+ public HostBean saveHost(HostBean host) {
+ long id;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ id = db.insert(TABLE_HOSTS, null, host.getValues());
+ }
+
+ host.setId(id);
+
+ return host;
+ }
+
+ /**
+ * Update a field in a host record.
+ */
+ public boolean updateFontSize(HostBean host) {
+ long id = host.getId();
+ if (id < 0)
+ return false;
+
+ ContentValues updates = new ContentValues();
+ updates.put(FIELD_HOST_FONTSIZE, host.getFontSize());
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ db.update(TABLE_HOSTS, updates, "_id = ?",
+ new String[] { String.valueOf(id) });
+
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete a specific host by its <code>_id</code> value.
+ */
+ public void deleteHost(HostBean host) {
+ if (host.getId() < 0)
+ return;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.delete(TABLE_HOSTS, "_id = ?", new String[] { String.valueOf(host.getId()) });
+ }
+ }
+
+ /**
+ * Return a cursor that contains information about all known hosts.
+ * @param sortColors If true, sort by color, otherwise sort by nickname.
+ */
+ public List<HostBean> getHosts(boolean sortColors) {
+ String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME;
+ List<HostBean> hosts;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null, null, null, null, null, sortField + " ASC");
+
+ hosts = createHostBeans(c);
+
+ c.close();
+ }
+
+ return hosts;
+ }
+
+ /**
+ * @param hosts
+ * @param c
+ */
+ private List<HostBean> createHostBeans(Cursor c) {
+ List<HostBean> hosts = new LinkedList<HostBean>();
+
+ final int COL_ID = c.getColumnIndexOrThrow("_id"),
+ COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_HOST_NICKNAME),
+ COL_PROTOCOL = c.getColumnIndexOrThrow(FIELD_HOST_PROTOCOL),
+ COL_USERNAME = c.getColumnIndexOrThrow(FIELD_HOST_USERNAME),
+ COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME),
+ COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT),
+ COL_LASTCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT),
+ COL_COLOR = c.getColumnIndexOrThrow(FIELD_HOST_COLOR),
+ COL_USEKEYS = c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS),
+ COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT),
+ COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN),
+ COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID),
+ COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION),
+ COL_DELKEY = c.getColumnIndexOrThrow(FIELD_HOST_DELKEY),
+ COL_FONTSIZE = c.getColumnIndexOrThrow(FIELD_HOST_FONTSIZE),
+ COL_COMPRESSION = c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION),
+ COL_ENCODING = c.getColumnIndexOrThrow(FIELD_HOST_ENCODING),
+ COL_STAYCONNECTED = c.getColumnIndexOrThrow(FIELD_HOST_STAYCONNECTED);
+
+
+ while (c.moveToNext()) {
+ HostBean host = new HostBean();
+
+ host.setId(c.getLong(COL_ID));
+ host.setNickname(c.getString(COL_NICKNAME));
+ host.setProtocol(c.getString(COL_PROTOCOL));
+ host.setUsername(c.getString(COL_USERNAME));
+ host.setHostname(c.getString(COL_HOSTNAME));
+ host.setPort(c.getInt(COL_PORT));
+ host.setLastConnect(c.getLong(COL_LASTCONNECT));
+ host.setColor(c.getString(COL_COLOR));
+ host.setUseKeys(Boolean.valueOf(c.getString(COL_USEKEYS)));
+ host.setUseAuthAgent(c.getString(COL_USEAUTHAGENT));
+ host.setPostLogin(c.getString(COL_POSTLOGIN));
+ host.setPubkeyId(c.getLong(COL_PUBKEYID));
+ host.setWantSession(Boolean.valueOf(c.getString(COL_WANTSESSION)));
+ host.setDelKey(c.getString(COL_DELKEY));
+ host.setFontSize(c.getInt(COL_FONTSIZE));
+ host.setCompression(Boolean.valueOf(c.getString(COL_COMPRESSION)));
+ host.setEncoding(c.getString(COL_ENCODING));
+ host.setStayConnected(Boolean.valueOf(c.getString(COL_STAYCONNECTED)));
+
+ hosts.add(host);
+ }
+
+ return hosts;
+ }
+
+ /**
+ * @param c
+ * @return
+ */
+ private HostBean getFirstHostBean(Cursor c) {
+ HostBean host = null;
+
+ List<HostBean> hosts = createHostBeans(c);
+ if (hosts.size() > 0)
+ host = hosts.get(0);
+
+ c.close();
+
+ return host;
+ }
+
+ /**
+ * @param nickname
+ * @param protocol
+ * @param username
+ * @param hostname
+ * @param hostname2
+ * @param port
+ * @return
+ */
+ public HostBean findHost(Map<String, String> selection) {
+ StringBuilder selectionBuilder = new StringBuilder();
+
+ Iterator<Entry<String, String>> i = selection.entrySet().iterator();
+
+ List<String> selectionValuesList = new LinkedList<String>();
+ int n = 0;
+ while (i.hasNext()) {
+ Entry<String, String> entry = i.next();
+
+ if (entry.getValue() == null)
+ continue;
+
+ if (n++ > 0)
+ selectionBuilder.append(" AND ");
+
+ selectionBuilder.append(entry.getKey())
+ .append(" = ?");
+
+ selectionValuesList.add(entry.getValue());
+ }
+
+ String selectionValues[] = new String[selectionValuesList.size()];
+ selectionValuesList.toArray(selectionValues);
+ selectionValuesList = null;
+
+ HostBean host;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null,
+ selectionBuilder.toString(),
+ selectionValues,
+ null, null, null);
+
+ host = getFirstHostBean(c);
+ }
+
+ return host;
+ }
+
+ /**
+ * @param hostId
+ * @return
+ */
+ public HostBean findHostById(long hostId) {
+ HostBean host;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null,
+ "_id = ?", new String[] { String.valueOf(hostId) },
+ null, null, null);
+
+ host = getFirstHostBean(c);
+ }
+
+ return host;
+ }
+
+ /**
+ * Record the given hostkey into database under this nickname.
+ * @param hostname
+ * @param port
+ * @param hostkeyalgo
+ * @param hostkey
+ */
+ public void saveKnownHost(String hostname, int port, String hostkeyalgo, byte[] hostkey) {
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_HOSTKEYALGO, hostkeyalgo);
+ values.put(FIELD_HOST_HOSTKEY, hostkey);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ db.update(TABLE_HOSTS, values,
+ FIELD_HOST_HOSTNAME + " = ? AND " + FIELD_HOST_PORT + " = ?",
+ new String[] { hostname, String.valueOf(port) });
+ Log.d(TAG, String.format("Finished saving hostkey information for '%s'", hostname));
+ }
+ }
+
+ /**
+ * Build list of known hosts for Trilead library.
+ * @return
+ */
+ public KnownHosts getKnownHosts() {
+ KnownHosts known = new KnownHosts();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_HOSTS, new String[] { FIELD_HOST_HOSTNAME,
+ FIELD_HOST_PORT, FIELD_HOST_HOSTKEYALGO, FIELD_HOST_HOSTKEY },
+ null, null, null, null, null);
+
+ if (c != null) {
+ int COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME),
+ COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT),
+ COL_HOSTKEYALGO = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEYALGO),
+ COL_HOSTKEY = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEY);
+
+ while (c.moveToNext()) {
+ String hostname = c.getString(COL_HOSTNAME),
+ hostkeyalgo = c.getString(COL_HOSTKEYALGO);
+ int port = c.getInt(COL_PORT);
+ byte[] hostkey = c.getBlob(COL_HOSTKEY);
+
+ if (hostkeyalgo == null || hostkeyalgo.length() == 0) continue;
+ if (hostkey == null || hostkey.length == 0) continue;
+
+ try {
+ known.addHostkey(new String[] { String.format("%s:%d", hostname, port) }, hostkeyalgo, hostkey);
+ } catch(Exception e) {
+ Log.e(TAG, "Problem while adding a known host from database", e);
+ }
+ }
+
+ c.close();
+ }
+ }
+
+ return known;
+ }
+
+ /**
+ * Unset any hosts using a pubkey ID that has been deleted.
+ * @param pubkeyId
+ */
+ public void stopUsingPubkey(long pubkeyId) {
+ if (pubkeyId < 0) return;
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_PUBKEYID, PUBKEYID_ANY);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ db.update(TABLE_HOSTS, values, FIELD_HOST_PUBKEYID + " = ?", new String[] { String.valueOf(pubkeyId) });
+ }
+
+ Log.d(TAG, String.format("Set all hosts using pubkey id %d to -1", pubkeyId));
+ }
+
+ /*
+ * Methods for dealing with port forwards attached to hosts
+ */
+
+ /**
+ * Returns a list of all the port forwards associated with a particular host ID.
+ * @param host the host for which we want the port forward list
+ * @return port forwards associated with host ID
+ */
+ public List<PortForwardBean> getPortForwardsForHost(HostBean host) {
+ List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+
+ Cursor c = db.query(TABLE_PORTFORWARDS, new String[] {
+ "_id", FIELD_PORTFORWARD_NICKNAME, FIELD_PORTFORWARD_TYPE, FIELD_PORTFORWARD_SOURCEPORT,
+ FIELD_PORTFORWARD_DESTADDR, FIELD_PORTFORWARD_DESTPORT },
+ FIELD_PORTFORWARD_HOSTID + " = ?", new String[] { String.valueOf(host.getId()) },
+ null, null, null);
+
+ while (c.moveToNext()) {
+ PortForwardBean pfb = new PortForwardBean(
+ c.getInt(0),
+ host.getId(),
+ c.getString(1),
+ c.getString(2),
+ c.getInt(3),
+ c.getString(4),
+ c.getInt(5));
+ portForwards.add(pfb);
+ }
+
+ c.close();
+ }
+
+ return portForwards;
+ }
+
+ /**
+ * Update the parameters of a port forward in the database.
+ * @param pfb {@link PortForwardBean} to save
+ * @return true on success
+ */
+ public boolean savePortForward(PortForwardBean pfb) {
+ boolean success = false;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ if (pfb.getId() < 0) {
+ long id = db.insert(TABLE_PORTFORWARDS, null, pfb.getValues());
+ pfb.setId(id);
+ success = true;
+ } else {
+ if (db.update(TABLE_PORTFORWARDS, pfb.getValues(), "_id = ?", new String[] { String.valueOf(pfb.getId()) }) > 0)
+ success = true;
+ }
+ }
+
+ return success;
+ }
+
+ /**
+ * Deletes a port forward from the database.
+ * @param pfb {@link PortForwardBean} to delete
+ */
+ public void deletePortForward(PortForwardBean pfb) {
+ if (pfb.getId() < 0)
+ return;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.delete(TABLE_PORTFORWARDS, "_id = ?", new String[] { String.valueOf(pfb.getId()) });
+ }
+ }
+
+ public Integer[] getColorsForScheme(int scheme) {
+ Integer[] colors = Colors.defaults.clone();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_COLORS, new String[] {
+ FIELD_COLOR_NUMBER, FIELD_COLOR_VALUE },
+ FIELD_COLOR_SCHEME + " = ?",
+ new String[] { String.valueOf(scheme) },
+ null, null, null);
+
+ while (c.moveToNext()) {
+ colors[c.getInt(0)] = new Integer(c.getInt(1));
+ }
+
+ c.close();
+ }
+
+ return colors;
+ }
+
+ public void setColorForScheme(int scheme, int number, int value) {
+ final SQLiteDatabase db;
+
+ final String[] whereArgs = new String[] { String.valueOf(scheme), String.valueOf(number) };
+
+ if (value == Colors.defaults[number]) {
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ db.delete(TABLE_COLORS,
+ WHERE_SCHEME_AND_COLOR, whereArgs);
+ }
+ } else {
+ final ContentValues values = new ContentValues();
+ values.put(FIELD_COLOR_VALUE, value);
+
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ final int rowsAffected = db.update(TABLE_COLORS, values,
+ WHERE_SCHEME_AND_COLOR, whereArgs);
+
+ if (rowsAffected == 0) {
+ values.put(FIELD_COLOR_SCHEME, scheme);
+ values.put(FIELD_COLOR_NUMBER, number);
+ db.insert(TABLE_COLORS, null, values);
+ }
+ }
+ }
+ }
+
+ public void setGlobalColor(int number, int value) {
+ setColorForScheme(DEFAULT_COLOR_SCHEME, number, value);
+ }
+
+ public int[] getDefaultColorsForScheme(int scheme) {
+ int[] colors = new int[] { DEFAULT_FG_COLOR, DEFAULT_BG_COLOR };
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_COLOR_DEFAULTS,
+ new String[] { FIELD_COLOR_FG, FIELD_COLOR_BG },
+ FIELD_COLOR_SCHEME + " = ?",
+ new String[] { String.valueOf(scheme) },
+ null, null, null);
+
+ if (c.moveToFirst()) {
+ colors[0] = c.getInt(0);
+ colors[1] = c.getInt(1);
+ }
+
+ c.close();
+ }
+
+ return colors;
+ }
+
+ public int[] getGlobalDefaultColors() {
+ return getDefaultColorsForScheme(DEFAULT_COLOR_SCHEME);
+ }
+
+ public void setDefaultColorsForScheme(int scheme, int fg, int bg) {
+ SQLiteDatabase db;
+
+ String schemeWhere = null;
+ String[] whereArgs;
+
+ schemeWhere = FIELD_COLOR_SCHEME + " = ?";
+ whereArgs = new String[] { String.valueOf(scheme) };
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_COLOR_FG, fg);
+ values.put(FIELD_COLOR_BG, bg);
+
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ int rowsAffected = db.update(TABLE_COLOR_DEFAULTS, values,
+ schemeWhere, whereArgs);
+
+ if (rowsAffected == 0) {
+ values.put(FIELD_COLOR_SCHEME, scheme);
+ db.insert(TABLE_COLOR_DEFAULTS, null, values);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java
new file mode 100644
index 0000000..ef33797
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java
@@ -0,0 +1,26 @@
+/*
+ * 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.util;
+
+/**
+ * @author kroot
+ *
+ */
+public interface OnDbWrittenListener {
+ public void onDbWritten();
+}
diff --git a/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java
new file mode 100644
index 0000000..5debd65
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.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.util;
+
+public interface OnEntropyGatheredListener {
+ void onEntropyGathered(byte[] entropy);
+}
diff --git a/app/src/main/java/org/connectbot/util/PreferenceConstants.java b/app/src/main/java/org/connectbot/util/PreferenceConstants.java
new file mode 100644
index 0000000..e9fb06c
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PreferenceConstants.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util;
+
+import android.os.Build;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class PreferenceConstants {
+ public static final int SDK_INT = Integer.parseInt(Build.VERSION.SDK);
+ public static final boolean PRE_ECLAIR = SDK_INT < 5;
+ public static final boolean PRE_FROYO = SDK_INT < 8;
+ public static final boolean PRE_HONEYCOMB = SDK_INT < 11;
+
+ public static final String MEMKEYS = "memkeys";
+ public static final String UPDATE = "update";
+
+ public static final String UPDATE_DAILY = "Daily";
+ public static final String UPDATE_WEEKLY = "Weekly";
+ public static final String UPDATE_NEVER = "Never";
+
+ public static final String LAST_CHECKED = "lastchecked";
+
+ public static final String SCROLLBACK = "scrollback";
+
+ public static final String EMULATION = "emulation";
+
+ public static final String ROTATION = "rotation";
+
+ public static final String ROTATION_DEFAULT = "Default";
+ public static final String ROTATION_LANDSCAPE = "Force landscape";
+ public static final String ROTATION_PORTRAIT = "Force portrait";
+ public static final String ROTATION_AUTOMATIC = "Automatic";
+
+ public static final String FULLSCREEN = "fullscreen";
+
+ public static final String KEYMODE = "keymode";
+
+ public static final String KEYMODE_RIGHT = "Use right-side keys";
+ public static final String KEYMODE_LEFT = "Use left-side keys";
+
+ public static final String CAMERA = "camera";
+
+ public static final String CAMERA_CTRLA_SPACE = "Ctrl+A then Space";
+ public static final String CAMERA_CTRLA = "Ctrl+A";
+ public static final String CAMERA_ESC = "Esc";
+ public static final String CAMERA_ESC_A = "Esc+A";
+
+ public static final String KEEP_ALIVE = "keepalive";
+
+ public static final String WIFI_LOCK = "wifilock";
+
+ public static final String BUMPY_ARROWS = "bumpyarrows";
+
+ public static final String EULA = "eula";
+
+ public static final String SORT_BY_COLOR = "sortByColor";
+
+ public static final String BELL = "bell";
+ public static final String BELL_VOLUME = "bellVolume";
+ public static final String BELL_VIBRATE = "bellVibrate";
+ public static final String BELL_NOTIFICATION = "bellNotification";
+ public static final float DEFAULT_BELL_VOLUME = 0.25f;
+
+ public static final String CONNECTION_PERSIST = "connPersist";
+
+ public static final String SHIFT_FKEYS = "shiftfkeys";
+ public static final String CTRL_FKEYS = "ctrlfkeys";
+ public static final String VOLUME_FONT = "volumefont";
+
+ /* Backup identifiers */
+ public static final String BACKUP_PREF_KEY = "prefs";
+}
diff --git a/app/src/main/java/org/connectbot/util/PubkeyDatabase.java b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java
new file mode 100644
index 0000000..a8993cb
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java
@@ -0,0 +1,329 @@
+/*
+ * 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.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.connectbot.bean.PubkeyBean;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+/**
+ * Public Key Encryption database. Contains private and public key pairs
+ * for public key authentication.
+ *
+ * @author Kenny Root
+ */
+public class PubkeyDatabase extends RobustSQLiteOpenHelper {
+ public final static String TAG = "ConnectBot.PubkeyDatabase";
+
+ public final static String DB_NAME = "pubkeys";
+ public final static int DB_VERSION = 2;
+
+ public final static String TABLE_PUBKEYS = "pubkeys";
+ public final static String FIELD_PUBKEY_NICKNAME = "nickname";
+ public final static String FIELD_PUBKEY_TYPE = "type";
+ public final static String FIELD_PUBKEY_PRIVATE = "private";
+ public final static String FIELD_PUBKEY_PUBLIC = "public";
+ public final static String FIELD_PUBKEY_ENCRYPTED = "encrypted";
+ public final static String FIELD_PUBKEY_STARTUP = "startup";
+ public final static String FIELD_PUBKEY_CONFIRMUSE = "confirmuse";
+ public final static String FIELD_PUBKEY_LIFETIME = "lifetime";
+
+ public final static String KEY_TYPE_RSA = "RSA",
+ KEY_TYPE_DSA = "DSA",
+ KEY_TYPE_IMPORTED = "IMPORTED",
+ KEY_TYPE_EC = "EC";
+
+ private Context context;
+
+ static {
+ addTableName(TABLE_PUBKEYS);
+ }
+
+ public PubkeyDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+
+ this.context = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ super.onCreate(db);
+
+ db.execSQL("CREATE TABLE " + TABLE_PUBKEYS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PUBKEY_NICKNAME + " TEXT, "
+ + FIELD_PUBKEY_TYPE + " TEXT, "
+ + FIELD_PUBKEY_PRIVATE + " BLOB, "
+ + FIELD_PUBKEY_PUBLIC + " BLOB, "
+ + FIELD_PUBKEY_ENCRYPTED + " INTEGER, "
+ + FIELD_PUBKEY_STARTUP + " INTEGER, "
+ + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0, "
+ + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0)");
+ }
+
+ @Override
+ public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException {
+ switch (oldVersion) {
+ case 1:
+ db.execSQL("ALTER TABLE " + TABLE_PUBKEYS
+ + " ADD COLUMN " + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE " + TABLE_PUBKEYS
+ + " ADD COLUMN " + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0");
+ }
+ }
+
+ /**
+ * Delete a specific host by its <code>_id</code> value.
+ */
+ public void deletePubkey(PubkeyBean pubkey) {
+ HostDatabase hostdb = new HostDatabase(context);
+ hostdb.stopUsingPubkey(pubkey.getId());
+ hostdb.close();
+
+ SQLiteDatabase db = getWritableDatabase();
+ db.delete(TABLE_PUBKEYS, "_id = ?", new String[] { Long.toString(pubkey.getId()) });
+ db.close();
+ }
+
+ /**
+ * Return a cursor that contains information about all known hosts.
+ */
+ /*
+ public Cursor allPubkeys() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ return db.query(TABLE_PUBKEYS, new String[] { "_id",
+ FIELD_PUBKEY_NICKNAME, FIELD_PUBKEY_TYPE, FIELD_PUBKEY_PRIVATE,
+ FIELD_PUBKEY_PUBLIC, FIELD_PUBKEY_ENCRYPTED, FIELD_PUBKEY_STARTUP },
+ null, null, null, null, null);
+ }*/
+
+ public List<PubkeyBean> allPubkeys() {
+ return getPubkeys(null, null);
+ }
+
+ public List<PubkeyBean> getAllStartPubkeys() {
+ return getPubkeys(FIELD_PUBKEY_STARTUP + " = 1 AND " + FIELD_PUBKEY_ENCRYPTED + " = 0", null);
+ }
+
+ private List<PubkeyBean> getPubkeys(String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ List<PubkeyBean> pubkeys = new LinkedList<PubkeyBean>();
+
+ Cursor c = db.query(TABLE_PUBKEYS, null, selection, selectionArgs, null, null, null);
+
+ if (c != null) {
+ final int COL_ID = c.getColumnIndexOrThrow("_id"),
+ COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME),
+ COL_TYPE = c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE),
+ COL_PRIVATE = c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE),
+ COL_PUBLIC = c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC),
+ COL_ENCRYPTED = c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED),
+ COL_STARTUP = c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP),
+ COL_CONFIRMUSE = c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE),
+ COL_LIFETIME = c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME);
+
+ while (c.moveToNext()) {
+ PubkeyBean pubkey = new PubkeyBean();
+
+ pubkey.setId(c.getLong(COL_ID));
+ pubkey.setNickname(c.getString(COL_NICKNAME));
+ pubkey.setType(c.getString(COL_TYPE));
+ pubkey.setPrivateKey(c.getBlob(COL_PRIVATE));
+ pubkey.setPublicKey(c.getBlob(COL_PUBLIC));
+ pubkey.setEncrypted(c.getInt(COL_ENCRYPTED) > 0);
+ pubkey.setStartup(c.getInt(COL_STARTUP) > 0);
+ pubkey.setConfirmUse(c.getInt(COL_CONFIRMUSE) > 0);
+ pubkey.setLifetime(c.getInt(COL_LIFETIME));
+
+ pubkeys.add(pubkey);
+ }
+
+ c.close();
+ }
+
+ db.close();
+
+ return pubkeys;
+ }
+
+ /**
+ * @param hostId
+ * @return
+ */
+ public PubkeyBean findPubkeyById(long pubkeyId) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_PUBKEYS, null,
+ "_id = ?", new String[] { String.valueOf(pubkeyId) },
+ null, null, null);
+
+ PubkeyBean pubkey = null;
+
+ if (c != null) {
+ if (c.moveToFirst())
+ pubkey = createPubkeyBean(c);
+
+ c.close();
+ }
+
+ db.close();
+
+ return pubkey;
+ }
+
+ private PubkeyBean createPubkeyBean(Cursor c) {
+ PubkeyBean pubkey = new PubkeyBean();
+
+ pubkey.setId(c.getLong(c.getColumnIndexOrThrow("_id")));
+ pubkey.setNickname(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME)));
+ pubkey.setType(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE)));
+ pubkey.setPrivateKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE)));
+ pubkey.setPublicKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC)));
+ pubkey.setEncrypted(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED)) > 0);
+ pubkey.setStartup(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP)) > 0);
+ pubkey.setConfirmUse(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE)) > 0);
+ pubkey.setLifetime(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME)));
+
+ return pubkey;
+ }
+
+ /**
+ * Pull all values for a given column as a list of Strings, probably for use
+ * in a ListPreference. Sorted by <code>_id</code> ascending.
+ */
+ public List<CharSequence> allValues(String column) {
+ List<CharSequence> list = new LinkedList<CharSequence>();
+
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id", column },
+ null, null, null, null, "_id ASC");
+
+ if (c != null) {
+ int COL = c.getColumnIndexOrThrow(column);
+
+ while (c.moveToNext())
+ list.add(c.getString(COL));
+
+ c.close();
+ }
+
+ db.close();
+
+ return list;
+ }
+
+ public String getNickname(long id) {
+ String nickname = null;
+
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id",
+ FIELD_PUBKEY_NICKNAME }, "_id = ?",
+ new String[] { Long.toString(id) }, null, null, null);
+
+ if (c != null) {
+ if (c.moveToFirst())
+ nickname = c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME));
+
+ c.close();
+ }
+
+ db.close();
+
+ return nickname;
+ }
+
+/*
+ public void setOnStart(long id, boolean onStart) {
+
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_PUBKEY_STARTUP, onStart ? 1 : 0);
+
+ db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { Long.toString(id) });
+
+ }
+
+ public boolean changePassword(long id, String oldPassword, String newPassword) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { FIELD_PUBKEY_TYPE,
+ FIELD_PUBKEY_PRIVATE, FIELD_PUBKEY_ENCRYPTED },
+ "_id = ?", new String[] { String.valueOf(id) },
+ null, null, null);
+
+ if (!c.moveToFirst())
+ return false;
+
+ String keyType = c.getString(0);
+ byte[] encPriv = c.getBlob(1);
+ c.close();
+
+ PrivateKey priv;
+ try {
+ priv = PubkeyUtils.decodePrivate(encPriv, keyType, oldPassword);
+ } catch (InvalidKeyException e) {
+ return false;
+ } catch (BadPaddingException e) {
+ return false;
+ } catch (InvalidKeySpecException e) {
+ return false;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_PUBKEY_PRIVATE, PubkeyUtils.getEncodedPrivate(priv, newPassword));
+ values.put(FIELD_PUBKEY_ENCRYPTED, newPassword.length() > 0 ? 1 : 0);
+ db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(id) });
+
+ return true;
+ }
+ */
+
+ /**
+ * @param pubkey
+ */
+ public PubkeyBean savePubkey(PubkeyBean pubkey) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ boolean success = false;
+
+ ContentValues values = pubkey.getValues();
+
+ if (pubkey.getId() > 0) {
+ values.remove("_id");
+ if (db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(pubkey.getId()) }) > 0)
+ success = true;
+ }
+
+ if (!success) {
+ long id = db.insert(TABLE_PUBKEYS, null, pubkey.getValues());
+ pubkey.setId(id);
+ }
+
+ db.close();
+
+ return pubkey;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/PubkeyUtils.java b/app/src/main/java/org/connectbot/util/PubkeyUtils.java
new file mode 100644
index 0000000..e7922bd
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PubkeyUtils.java
@@ -0,0 +1,352 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.keyczar.jce.EcCore;
+
+import android.util.Log;
+
+import com.trilead.ssh2.crypto.Base64;
+import com.trilead.ssh2.crypto.SimpleDERReader;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.ECDSASHA2Verify;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+
+public class PubkeyUtils {
+ private static final String TAG = "PubkeyUtils";
+
+ public static final String PKCS8_START = "-----BEGIN PRIVATE KEY-----";
+ public static final String PKCS8_END = "-----END PRIVATE KEY-----";
+
+ // Size in bytes of salt to use.
+ private static final int SALT_SIZE = 8;
+
+ // Number of iterations for password hashing. PKCS#5 recommends 1000
+ private static final int ITERATIONS = 1000;
+
+ // Cannot be instantiated
+ private PubkeyUtils() {
+ }
+
+ public static String formatKey(Key key){
+ String algo = key.getAlgorithm();
+ String fmt = key.getFormat();
+ byte[] encoded = key.getEncoded();
+ return "Key[algorithm=" + algo + ", format=" + fmt +
+ ", bytes=" + encoded.length + "]";
+ }
+
+ public static byte[] sha256(byte[] data) throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance("SHA-256").digest(data);
+ }
+
+ public static byte[] cipher(int mode, byte[] data, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ SecretKeySpec secretKeySpec = new SecretKeySpec(sha256(secret), "AES");
+ Cipher c = Cipher.getInstance("AES");
+ c.init(mode, secretKeySpec);
+ return c.doFinal(data);
+ }
+
+ public static byte[] encrypt(byte[] cleartext, String secret) throws Exception {
+ byte[] salt = new byte[SALT_SIZE];
+
+ byte[] ciphertext = Encryptor.encrypt(salt, ITERATIONS, secret, cleartext);
+
+ byte[] complete = new byte[salt.length + ciphertext.length];
+
+ System.arraycopy(salt, 0, complete, 0, salt.length);
+ System.arraycopy(ciphertext, 0, complete, salt.length, ciphertext.length);
+
+ Arrays.fill(salt, (byte) 0x00);
+ Arrays.fill(ciphertext, (byte) 0x00);
+
+ return complete;
+ }
+
+ public static byte[] decrypt(byte[] saltAndCiphertext, String secret) throws Exception {
+ try {
+ byte[] salt = new byte[SALT_SIZE];
+ byte[] ciphertext = new byte[saltAndCiphertext.length - salt.length];
+
+ System.arraycopy(saltAndCiphertext, 0, salt, 0, salt.length);
+ System.arraycopy(saltAndCiphertext, salt.length, ciphertext, 0, ciphertext.length);
+
+ return Encryptor.decrypt(salt, ITERATIONS, secret, ciphertext);
+ } catch (Exception e) {
+ Log.d("decrypt", "Could not decrypt with new method", e);
+ // We might be using the old encryption method.
+ return cipher(Cipher.DECRYPT_MODE, saltAndCiphertext, secret.getBytes());
+ }
+ }
+
+ public static byte[] getEncodedPrivate(PrivateKey pk, String secret) throws Exception {
+ final byte[] encoded = pk.getEncoded();
+ if (secret == null || secret.length() == 0) {
+ return encoded;
+ }
+ return encrypt(pk.getEncoded(), secret);
+ }
+
+ public static PrivateKey decodePrivate(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded);
+ KeyFactory kf = KeyFactory.getInstance(keyType);
+ return kf.generatePrivate(privKeySpec);
+ }
+
+ public static PrivateKey decodePrivate(byte[] encoded, String keyType, String secret) throws Exception {
+ if (secret != null && secret.length() > 0)
+ return decodePrivate(decrypt(encoded, secret), keyType);
+ else
+ return decodePrivate(encoded, keyType);
+ }
+
+ public static PublicKey decodePublic(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encoded);
+ KeyFactory kf = KeyFactory.getInstance(keyType);
+ return kf.generatePublic(pubKeySpec);
+ }
+
+ static String getAlgorithmForOid(String oid) throws NoSuchAlgorithmException {
+ if ("1.2.840.10045.2.1".equals(oid)) {
+ return "EC";
+ } else if ("1.2.840.113549.1.1.1".equals(oid)) {
+ return "RSA";
+ } else if ("1.2.840.10040.4.1".equals(oid)) {
+ return "DSA";
+ } else {
+ throw new NoSuchAlgorithmException("Unknown algorithm OID " + oid);
+ }
+ }
+
+ static String getOidFromPkcs8Encoded(byte[] encoded) throws NoSuchAlgorithmException {
+ if (encoded == null) {
+ throw new NoSuchAlgorithmException("encoding is null");
+ }
+
+ try {
+ SimpleDERReader reader = new SimpleDERReader(encoded);
+ reader.resetInput(reader.readSequenceAsByteArray());
+ reader.readInt();
+ reader.resetInput(reader.readSequenceAsByteArray());
+ return reader.readOid();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not read OID", e);
+ throw new NoSuchAlgorithmException("Could not read key", e);
+ }
+ }
+
+ public static KeyPair recoverKeyPair(byte[] encoded) throws NoSuchAlgorithmException,
+ InvalidKeySpecException {
+ final String algo = getAlgorithmForOid(getOidFromPkcs8Encoded(encoded));
+
+ final KeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded);
+
+ final KeyFactory kf = KeyFactory.getInstance(algo);
+ final PrivateKey priv = kf.generatePrivate(privKeySpec);
+
+ return new KeyPair(recoverPublicKey(kf, priv), priv);
+ }
+
+ static PublicKey recoverPublicKey(KeyFactory kf, PrivateKey priv)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (priv instanceof RSAPrivateCrtKey) {
+ RSAPrivateCrtKey rsaPriv = (RSAPrivateCrtKey) priv;
+ return kf.generatePublic(new RSAPublicKeySpec(rsaPriv.getModulus(), rsaPriv
+ .getPublicExponent()));
+ } else if (priv instanceof DSAPrivateKey) {
+ DSAPrivateKey dsaPriv = (DSAPrivateKey) priv;
+ DSAParams params = dsaPriv.getParams();
+
+ // Calculate public key Y
+ BigInteger y = params.getG().modPow(dsaPriv.getX(), params.getP());
+
+ return kf.generatePublic(new DSAPublicKeySpec(y, params.getP(), params.getQ(), params
+ .getG()));
+ } else if (priv instanceof ECPrivateKey) {
+ ECPrivateKey ecPriv = (ECPrivateKey) priv;
+ ECParameterSpec params = ecPriv.getParams();
+
+ // Calculate public key Y
+ ECPoint generator = params.getGenerator();
+ BigInteger[] wCoords = EcCore.multiplyPointA(new BigInteger[] { generator.getAffineX(),
+ generator.getAffineY() }, ecPriv.getS(), params);
+ ECPoint w = new ECPoint(wCoords[0], wCoords[1]);
+
+ return kf.generatePublic(new ECPublicKeySpec(w, params));
+ } else {
+ throw new NoSuchAlgorithmException("Key type must be RSA, DSA, or EC");
+ }
+ }
+
+ /*
+ * OpenSSH compatibility methods
+ */
+
+ public static String convertToOpenSSHFormat(PublicKey pk, String origNickname) throws IOException, InvalidKeyException {
+ String nickname = origNickname;
+ if (nickname == null)
+ nickname = "connectbot@android";
+
+ if (pk instanceof RSAPublicKey) {
+ String data = "ssh-rsa ";
+ data += String.valueOf(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pk)));
+ return data + " " + nickname;
+ } else if (pk instanceof DSAPublicKey) {
+ String data = "ssh-dss ";
+ data += String.valueOf(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pk)));
+ return data + " " + nickname;
+ } else if (pk instanceof ECPublicKey) {
+ ECPublicKey ecPub = (ECPublicKey) pk;
+ String keyType = ECDSASHA2Verify.getCurveName(ecPub.getParams().getCurve().getField().getFieldSize());
+ String keyData = String.valueOf(Base64.encode(ECDSASHA2Verify.encodeSSHECDSAPublicKey(ecPub)));
+ return ECDSASHA2Verify.ECDSA_SHA2_PREFIX + keyType + " " + keyData + " " + nickname;
+ }
+
+ throw new InvalidKeyException("Unknown key type");
+ }
+
+ /*
+ * OpenSSH compatibility methods
+ */
+
+ /**
+ * @param trileadKey
+ * @return OpenSSH-encoded pubkey
+ */
+ public static byte[] extractOpenSSHPublic(KeyPair pair) {
+ try {
+ PublicKey pubKey = pair.getPublic();
+ if (pubKey instanceof RSAPublicKey) {
+ return RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pair.getPublic());
+ } else if (pubKey instanceof DSAPublicKey) {
+ return DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pair.getPublic());
+ } else if (pubKey instanceof ECPublicKey) {
+ return ECDSASHA2Verify.encodeSSHECDSAPublicKey((ECPublicKey) pair.getPublic());
+ } else {
+ return null;
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static String exportPEM(PrivateKey key, String secret) throws NoSuchAlgorithmException, InvalidParameterSpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, InvalidKeySpecException, IllegalBlockSizeException, IOException {
+ StringBuilder sb = new StringBuilder();
+
+ byte[] data = key.getEncoded();
+
+ sb.append(PKCS8_START);
+ sb.append('\n');
+
+ if (secret != null) {
+ byte[] salt = new byte[8];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(salt);
+
+ PBEParameterSpec defParams = new PBEParameterSpec(salt, 1);
+ AlgorithmParameters params = AlgorithmParameters.getInstance(key.getAlgorithm());
+
+ params.init(defParams);
+
+ PBEKeySpec pbeSpec = new PBEKeySpec(secret.toCharArray());
+
+ SecretKeyFactory keyFact = SecretKeyFactory.getInstance(key.getAlgorithm());
+ Cipher cipher = Cipher.getInstance(key.getAlgorithm());
+ cipher.init(Cipher.WRAP_MODE, keyFact.generateSecret(pbeSpec), params);
+
+ byte[] wrappedKey = cipher.wrap(key);
+
+ EncryptedPrivateKeyInfo pinfo = new EncryptedPrivateKeyInfo(params, wrappedKey);
+
+ data = pinfo.getEncoded();
+
+ sb.append("Proc-Type: 4,ENCRYPTED\n");
+ sb.append("DEK-Info: DES-EDE3-CBC,");
+ sb.append(encodeHex(salt));
+ sb.append("\n\n");
+ }
+
+ int i = sb.length();
+ sb.append(Base64.encode(data));
+ for (i += 63; i < sb.length(); i += 64) {
+ sb.insert(i, "\n");
+ }
+
+ sb.append('\n');
+ sb.append(PKCS8_END);
+ sb.append('\n');
+
+ return sb.toString();
+ }
+
+ private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6',
+ '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ protected static String encodeHex(byte[] bytes) {
+ final char[] hex = new char[bytes.length * 2];
+
+ int i = 0;
+ for (byte b : bytes) {
+ hex[i++] = HEX_DIGITS[(b >> 4) & 0x0f];
+ hex[i++] = HEX_DIGITS[b & 0x0f];
+ }
+
+ return String.valueOf(hex);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java
new file mode 100644
index 0000000..abdd991
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java
@@ -0,0 +1,133 @@
+/*
+ * 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.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class RobustSQLiteOpenHelper extends SQLiteOpenHelper {
+ private static List<String> mTableNames = new LinkedList<String>();
+ private static List<String> mIndexNames = new LinkedList<String>();
+
+ public RobustSQLiteOpenHelper(Context context, String name,
+ CursorFactory factory, int version) {
+ super(context, name, factory, version);
+ }
+
+ protected static void addTableName(String tableName) {
+ mTableNames.add(tableName);
+ }
+
+ protected static void addIndexName(String indexName) {
+ mIndexNames.add(indexName);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ dropAllTables(db);
+ }
+
+ @Override
+ public final void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ onRobustUpgrade(db, oldVersion, newVersion);
+ } catch (SQLiteException e) {
+ // The database has entered an unknown state. Try to recover.
+ try {
+ regenerateTables(db);
+ } catch (SQLiteException e2) {
+ dropAndCreateTables(db);
+ }
+ }
+ }
+
+ public abstract void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException;
+
+ private void regenerateTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "OLD_");
+
+ for (String tableName : mTableNames)
+ db.execSQL("ALTER TABLE " + tableName + " RENAME TO OLD_"
+ + tableName);
+
+ onCreate(db);
+
+ for (String tableName : mTableNames)
+ repopulateTable(db, tableName);
+
+ dropAllTablesWithPrefix(db, "OLD_");
+ }
+
+ private void repopulateTable(SQLiteDatabase db, String tableName) {
+ String columns = getTableColumnNames(db, tableName);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("INSERT INTO ")
+ .append(tableName)
+ .append(" (")
+ .append(columns)
+ .append(") SELECT ")
+ .append(columns)
+ .append(" FROM OLD_")
+ .append(tableName);
+
+ String sql = sb.toString();
+ db.execSQL(sql);
+ }
+
+ private String getTableColumnNames(SQLiteDatabase db, String tableName) {
+ StringBuilder sb = new StringBuilder();
+
+ Cursor fields = db.rawQuery("PRAGMA table_info(" + tableName + ")", null);
+ while (fields.moveToNext()) {
+ if (!fields.isFirst())
+ sb.append(", ");
+ sb.append(fields.getString(1));
+ }
+ fields.close();
+
+ return sb.toString();
+ }
+
+ private void dropAndCreateTables(SQLiteDatabase db) {
+ dropAllTables(db);
+ onCreate(db);
+ }
+
+ private void dropAllTablesWithPrefix(SQLiteDatabase db, String prefix) {
+ for (String indexName : mIndexNames)
+ db.execSQL("DROP INDEX IF EXISTS " + prefix + indexName);
+ for (String tableName : mTableNames)
+ db.execSQL("DROP TABLE IF EXISTS " + prefix + tableName);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "");
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java
new file mode 100644
index 0000000..2c01b30
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * 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.
+ */
+
+/*
+ * 090408
+ * Keith Wiley
+ * kwiley@keithwiley.com
+ * http://keithwiley.com
+ *
+ * UberColorPickerDialog v1.1
+ *
+ * This color picker was implemented as a (significant) extension of the
+ * ColorPickerDialog class provided in the Android API Demos. You are free
+ * to drop it unchanged into your own projects or to modify it as you see
+ * fit. I would appreciate it if this comment block were let intact,
+ * merely for credit's sake.
+ *
+ * Enjoy!
+ */
+
+package org.connectbot.util;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ComposeShader;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * UberColorPickerDialog is a seriously enhanced version of the UberColorPickerDialog
+ * class provided in the Android API Demos.<p>
+ *
+ * NOTE (from Kenny Root): This is a VERY slimmed down version custom for ConnectBot.
+ * Visit Keith's site for the full version at the URL listed in the author line.<p>
+ *
+ * @author Keith Wiley, kwiley@keithwiley.com, http://keithwiley.com
+ */
+public class UberColorPickerDialog extends Dialog {
+ private final OnColorChangedListener mListener;
+ private final int mInitialColor;
+
+ /**
+ * Callback to the creator of the dialog, informing the creator of a new color and notifying that the dialog is about to dismiss.
+ */
+ public interface OnColorChangedListener {
+ void colorChanged(int color);
+ }
+
+ /**
+ * Ctor
+ * @param context
+ * @param listener
+ * @param initialColor
+ * @param showTitle If true, a title is shown across the top of the dialog. If false a toast is shown instead.
+ */
+ public UberColorPickerDialog(Context context,
+ OnColorChangedListener listener,
+ int initialColor) {
+ super(context);
+
+ mListener = listener;
+ mInitialColor = initialColor;
+ }
+
+ /**
+ * Activity entry point
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ OnColorChangedListener l = new OnColorChangedListener() {
+ public void colorChanged(int color) {
+ mListener.colorChanged(color);
+ dismiss();
+ }
+ };
+
+ DisplayMetrics dm = new DisplayMetrics();
+ getWindow().getWindowManager().getDefaultDisplay().getMetrics(dm);
+ int screenWidth = dm.widthPixels;
+ int screenHeight = dm.heightPixels;
+
+ setTitle("Pick a color (try the trackball)");
+
+ try {
+ setContentView(new ColorPickerView(getContext(), l, screenWidth, screenHeight, mInitialColor));
+ }
+ catch (Exception e) {
+ //There is currently only one kind of ctor exception, that where no methods are enabled.
+ dismiss(); //This doesn't work! The dialog is still shown (its title at least, the layout is empty from the exception being thrown). <sigh>
+ }
+ }
+
+ /**
+ * ColorPickerView is the meat of this color picker (as opposed to the enclosing class).
+ * All the heavy lifting is done directly by this View subclass.
+ * <P>
+ * You can enable/disable whichever color chooser methods you want by modifying the ENABLED_METHODS switches. They *should*
+ * do all the work required to properly enable/disable methods without losing track of what goes with what and what maps to what.
+ * <P>
+ * If you add a new color chooser method, do a text search for "NEW_METHOD_WORK_NEEDED_HERE". That tag indicates all
+ * the locations in the code that will have to be amended in order to properly add a new color chooser method.
+ * I highly recommend adding new methods to the end of the list. If you want to try to reorder the list, you're on your own.
+ */
+ private static class ColorPickerView extends View {
+ private static int SWATCH_WIDTH = 95;
+ private static final int SWATCH_HEIGHT = 60;
+
+ private static int PALETTE_POS_X = 0;
+ private static int PALETTE_POS_Y = SWATCH_HEIGHT;
+ private static final int PALETTE_DIM = SWATCH_WIDTH * 2;
+ private static final int PALETTE_RADIUS = PALETTE_DIM / 2;
+ private static final int PALETTE_CENTER_X = PALETTE_RADIUS;
+ private static final int PALETTE_CENTER_Y = PALETTE_RADIUS;
+
+ private static final int SLIDER_THICKNESS = 40;
+
+ private static int VIEW_DIM_X = PALETTE_DIM;
+ private static int VIEW_DIM_Y = SWATCH_HEIGHT;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ private static final int METHOD_HS_V_PALETTE = 0;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add a new entry to the list for each controller in the new method
+ private static final int TRACKED_NONE = -1; //No object on screen is currently being tracked
+ private static final int TRACK_SWATCH_OLD = 10;
+ private static final int TRACK_SWATCH_NEW = 11;
+ private static final int TRACK_HS_PALETTE = 30;
+ private static final int TRACK_VER_VALUE_SLIDER = 31;
+
+ private static final int TEXT_SIZE = 12;
+ private static int[] TEXT_HSV_POS = new int[2];
+ private static int[] TEXT_RGB_POS = new int[2];
+ private static int[] TEXT_YUV_POS = new int[2];
+ private static int[] TEXT_HEX_POS = new int[2];
+
+ private static final float PI = 3.141592653589793f;
+
+ private int mMethod = METHOD_HS_V_PALETTE;
+ private int mTracking = TRACKED_NONE; //What object on screen is currently being tracked for movement
+
+ //Zillions of persistant Paint objecs for drawing the View
+
+ private Paint mSwatchOld, mSwatchNew;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the palettes of the new method's UI controllers
+ private Paint mOvalHueSat;
+
+ private Bitmap mVerSliderBM;
+ private Canvas mVerSliderCv;
+
+ private Bitmap[] mHorSlidersBM = new Bitmap[3];
+ private Canvas[] mHorSlidersCv = new Canvas[3];
+
+ private Paint mValDimmer;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the icon for the new method
+ private Paint mOvalHueSatSmall;
+
+ private Paint mPosMarker;
+ private Paint mText;
+
+ private Rect mOldSwatchRect = new Rect();
+ private Rect mNewSwatchRect = new Rect();
+ private Rect mPaletteRect = new Rect();
+ private Rect mVerSliderRect = new Rect();
+
+ private int[] mSpectrumColorsRev;
+ private int mOriginalColor = 0; //The color passed in at the beginning, which can be reverted to at any time by tapping the old swatch.
+ private float[] mHSV = new float[3];
+ private int[] mRGB = new int[3];
+ private float[] mYUV = new float[3];
+ private String mHexStr = "";
+ private boolean mHSVenabled = true; //Only true if an HSV method is enabled
+ private boolean mRGBenabled = true; //Only true if an RGB method is enabled
+ private boolean mYUVenabled = true; //Only true if a YUV method is enabled
+ private boolean mHexenabled = true; //Only true if an RGB method is enabled
+ private int[] mCoord = new int[3]; //For drawing slider/palette markers
+ private int mFocusedControl = -1; //Which control receives trackball events.
+ private OnColorChangedListener mListener;
+
+ /**
+ * Ctor.
+ * @param c
+ * @param l
+ * @param width Used to determine orientation and adjust layout accordingly
+ * @param height Used to determine orientation and adjust layout accordingly
+ * @param color The initial color
+ * @throws Exception
+ */
+ ColorPickerView(Context c, OnColorChangedListener l, int width, int height, int color)
+ throws Exception {
+ super(c);
+
+ //We need to make the dialog focusable to retrieve trackball events.
+ setFocusable(true);
+
+ mListener = l;
+
+ mOriginalColor = color;
+
+ Color.colorToHSV(color, mHSV);
+
+ updateAllFromHSV();
+
+ //Setup the layout based on whether this is a portrait or landscape orientation.
+ if (width <= height) { //Portrait layout
+ SWATCH_WIDTH = (PALETTE_DIM + SLIDER_THICKNESS) / 2;
+
+ PALETTE_POS_X = 0;
+ PALETTE_POS_Y = TEXT_SIZE * 4 + SWATCH_HEIGHT;
+
+ //Set more rects, lots of rects
+ mOldSwatchRect.set(0, TEXT_SIZE * 4, SWATCH_WIDTH, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+ mNewSwatchRect.set(SWATCH_WIDTH, TEXT_SIZE * 4, SWATCH_WIDTH * 2, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+ mPaletteRect.set(0, PALETTE_POS_Y, PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+ mVerSliderRect.set(PALETTE_DIM, PALETTE_POS_Y, PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+ TEXT_HSV_POS[0] = 3;
+ TEXT_HSV_POS[1] = 0;
+ TEXT_RGB_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_RGB_POS[1] = TEXT_HSV_POS[1];
+ TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 100;
+ TEXT_YUV_POS[1] = TEXT_HSV_POS[1];
+ TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 150;
+ TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+ VIEW_DIM_X = PALETTE_DIM + SLIDER_THICKNESS;
+ VIEW_DIM_Y = SWATCH_HEIGHT + PALETTE_DIM + TEXT_SIZE * 4;
+ }
+ else { //Landscape layout
+ SWATCH_WIDTH = 110;
+
+ PALETTE_POS_X = SWATCH_WIDTH;
+ PALETTE_POS_Y = 0;
+
+ //Set more rects, lots of rects
+ mOldSwatchRect.set(0, TEXT_SIZE * 7, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT);
+ mNewSwatchRect.set(0, TEXT_SIZE * 7 + SWATCH_HEIGHT, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT * 2);
+ mPaletteRect.set(SWATCH_WIDTH, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+ mVerSliderRect.set(SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+ TEXT_HSV_POS[0] = 3;
+ TEXT_HSV_POS[1] = 0;
+ TEXT_RGB_POS[0] = TEXT_HSV_POS[0];
+ TEXT_RGB_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+ TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_YUV_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+ TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+ VIEW_DIM_X = PALETTE_POS_X + PALETTE_DIM + SLIDER_THICKNESS;
+ VIEW_DIM_Y = Math.max(mNewSwatchRect.bottom, PALETTE_DIM);
+ }
+
+ //Rainbows make everybody happy!
+ mSpectrumColorsRev = new int[] {
+ 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF,
+ 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000,
+ };
+
+ //Setup all the Paint and Shader objects. There are lots of them!
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the palettes of the new method's UI controllers
+
+ mSwatchOld = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSwatchOld.setStyle(Paint.Style.FILL);
+ mSwatchOld.setColor(Color.HSVToColor(mHSV));
+
+ mSwatchNew = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSwatchNew.setStyle(Paint.Style.FILL);
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ Shader shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+ Shader shaderB = new RadialGradient(0, 0, PALETTE_CENTER_X, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+ Shader shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+ mOvalHueSat = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mOvalHueSat.setShader(shader);
+ mOvalHueSat.setStyle(Paint.Style.FILL);
+ mOvalHueSat.setDither(true);
+
+ mVerSliderBM = Bitmap.createBitmap(SLIDER_THICKNESS, PALETTE_DIM, Bitmap.Config.RGB_565);
+ mVerSliderCv = new Canvas(mVerSliderBM);
+
+ for (int i = 0; i < 3; i++) {
+ mHorSlidersBM[i] = Bitmap.createBitmap(PALETTE_DIM, SLIDER_THICKNESS, Bitmap.Config.RGB_565);
+ mHorSlidersCv[i] = new Canvas(mHorSlidersBM[i]);
+ }
+
+ mValDimmer = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mValDimmer.setStyle(Paint.Style.FILL);
+ mValDimmer.setDither(true);
+ mValDimmer.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+
+ //Whew, we're done making the big Paints and Shaders for the swatches, palettes, and sliders.
+ //Now we need to make the Paints and Shaders that will draw the little method icons in the method selector list.
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the icon for the new method
+
+ shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+ shaderB = new RadialGradient(0, 0, PALETTE_DIM / 2, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+ shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+ mOvalHueSatSmall = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mOvalHueSatSmall.setShader(shader);
+ mOvalHueSatSmall.setStyle(Paint.Style.FILL);
+
+ //Make a simple stroking Paint for drawing markers and borders and stuff like that.
+ mPosMarker = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPosMarker.setStyle(Paint.Style.STROKE);
+ mPosMarker.setStrokeWidth(2);
+
+ //Make a basic text Paint.
+ mText = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mText.setTextSize(TEXT_SIZE);
+ mText.setColor(Color.WHITE);
+
+ //Kickstart
+ initUI();
+ }
+
+ /**
+ * Draw the entire view (the entire dialog).
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ //Draw the old and new swatches
+ drawSwatches(canvas);
+
+ //Write the text
+ writeColorParams(canvas);
+
+ //Draw the palette and sliders (the UI)
+ if (mMethod == METHOD_HS_V_PALETTE)
+ drawHSV1Palette(canvas);
+ }
+
+ /**
+ * Draw the old and new swatches.
+ * @param canvas
+ */
+ private void drawSwatches(Canvas canvas) {
+ float[] hsv = new float[3];
+
+ mText.setTextSize(16);
+
+ //Draw the original swatch
+ canvas.drawRect(mOldSwatchRect, mSwatchOld);
+ Color.colorToHSV(mOriginalColor, hsv);
+ //if (UberColorPickerDialog.isGray(mColor)) //Don't need this right here, but imp't to note
+ // hsv[1] = 0;
+ if (hsv[2] > .5)
+ mText.setColor(Color.BLACK);
+ canvas.drawText("Revert", mOldSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Revert") / 2, mOldSwatchRect.top + 16, mText);
+ mText.setColor(Color.WHITE);
+
+ //Draw the new swatch
+ canvas.drawRect(mNewSwatchRect, mSwatchNew);
+ if (mHSV[2] > .5)
+ mText.setColor(Color.BLACK);
+ canvas.drawText("Accept", mNewSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Accept") / 2, mNewSwatchRect.top + 16, mText);
+ mText.setColor(Color.WHITE);
+
+ mText.setTextSize(TEXT_SIZE);
+ }
+
+ /**
+ * Write the color parametes (HSV, RGB, YUV, Hex, etc.).
+ * @param canvas
+ */
+ private void writeColorParams(Canvas canvas) {
+ if (mHSVenabled) {
+ canvas.drawText("H: " + Integer.toString((int)(mHSV[0] / 360.0f * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("S: " + Integer.toString((int)(mHSV[1] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("V: " + Integer.toString((int)(mHSV[2] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mRGBenabled) {
+ canvas.drawText("R: " + mRGB[0], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("G: " + mRGB[1], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("B: " + mRGB[2], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mYUVenabled) {
+ canvas.drawText("Y: " + Integer.toString((int)(mYUV[0] * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("U: " + Integer.toString((int)((mYUV[1] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("V: " + Integer.toString((int)((mYUV[2] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mHexenabled)
+ canvas.drawText("#" + mHexStr, TEXT_HEX_POS[0], TEXT_HEX_POS[1] + TEXT_SIZE, mText);
+ }
+
+ /**
+ * Place a small circle on the 2D palette to indicate the current values.
+ * @param canvas
+ * @param markerPosX
+ * @param markerPosY
+ */
+ private void mark2DPalette(Canvas canvas, int markerPosX, int markerPosY) {
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawOval(new RectF(markerPosX - 5, markerPosY - 5, markerPosX + 5, markerPosY + 5), mPosMarker);
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawOval(new RectF(markerPosX - 3, markerPosY - 3, markerPosX + 3, markerPosY + 3), mPosMarker);
+ }
+
+ /**
+ * Draw a line across the slider to indicate its current value.
+ * @param canvas
+ * @param markerPos
+ */
+ private void markVerSlider(Canvas canvas, int markerPos) {
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawRect(new Rect(0, markerPos - 2, SLIDER_THICKNESS, markerPos + 3), mPosMarker);
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawRect(new Rect(0, markerPos, SLIDER_THICKNESS, markerPos + 1), mPosMarker);
+ }
+
+ /**
+ * Frame the slider to indicate that it has trackball focus.
+ * @param canvas
+ */
+ private void hilightFocusedVerSlider(Canvas canvas) {
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawRect(new Rect(0, 0, SLIDER_THICKNESS, PALETTE_DIM), mPosMarker);
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawRect(new Rect(2, 2, SLIDER_THICKNESS - 2, PALETTE_DIM - 2), mPosMarker);
+ }
+
+ /**
+ * Frame the 2D palette to indicate that it has trackball focus.
+ * @param canvas
+ */
+ private void hilightFocusedOvalPalette(Canvas canvas) {
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mPosMarker);
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS + 2, -PALETTE_RADIUS + 2, PALETTE_RADIUS - 2, PALETTE_RADIUS - 2), mPosMarker);
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate the basic draw functions here. Use the 2D palette or 1D sliders as templates for the new method.
+ /**
+ * Draw the UI for HSV with angular H and radial S combined in 2D and a 1D V slider.
+ * @param canvas
+ */
+ private void drawHSV1Palette(Canvas canvas) {
+ canvas.save();
+
+ canvas.translate(PALETTE_POS_X, PALETTE_POS_Y);
+
+ //Draw the 2D palette
+ canvas.translate(PALETTE_CENTER_X, PALETTE_CENTER_Y);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mOvalHueSat);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mValDimmer);
+ if (mFocusedControl == 0)
+ hilightFocusedOvalPalette(canvas);
+ mark2DPalette(canvas, mCoord[0], mCoord[1]);
+ canvas.translate(-PALETTE_CENTER_X, -PALETTE_CENTER_Y);
+
+ //Draw the 1D slider
+ canvas.translate(PALETTE_DIM, 0);
+ canvas.drawBitmap(mVerSliderBM, 0, 0, null);
+ if (mFocusedControl == 1)
+ hilightFocusedVerSlider(canvas);
+ markVerSlider(canvas, mCoord[2]);
+
+ canvas.restore();
+ }
+
+ /**
+ * Initialize the current color chooser's UI (set its color parameters and set its palette and slider values accordingly).
+ */
+ private void initUI() {
+ initHSV1Palette();
+
+ //Focus on the first controller (arbitrary).
+ mFocusedControl = 0;
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last init function shown below
+ /**
+ * Initialize a color chooser.
+ */
+ private void initHSV1Palette() {
+ setOvalValDimmer();
+ setVerValSlider();
+
+ float angle = 2*PI - mHSV[0] / (180 / 3.1415927f);
+ float radius = mHSV[1] * PALETTE_RADIUS;
+ mCoord[0] = (int)(Math.cos(angle) * radius);
+ mCoord[1] = (int)(Math.sin(angle) * radius);
+
+ mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the set functions below, one per UI controller in the new method
+ /**
+ * Adjust a Paint which, when painted, dims its underlying object to show the effects of varying value (brightness).
+ */
+ private void setOvalValDimmer() {
+ float[] hsv = new float[3];
+ hsv[0] = mHSV[0];
+ hsv[1] = 0;
+ hsv[2] = mHSV[2];
+ int gray = Color.HSVToColor(hsv);
+ mValDimmer.setColor(gray);
+ }
+
+ /**
+ * Create a linear gradient shader to show variations in value.
+ */
+ private void setVerValSlider() {
+ float[] hsv = new float[3];
+ hsv[0] = mHSV[0];
+ hsv[1] = mHSV[1];
+ hsv[2] = 1;
+ int col = Color.HSVToColor(hsv);
+
+ int colors[] = new int[2];
+ colors[0] = col;
+ colors[1] = 0xFF000000;
+ GradientDrawable gradDraw = new GradientDrawable(Orientation.TOP_BOTTOM, colors);
+ gradDraw.setDither(true);
+ gradDraw.setLevel(10000);
+ gradDraw.setBounds(0, 0, SLIDER_THICKNESS, PALETTE_DIM);
+ gradDraw.draw(mVerSliderCv);
+ }
+
+ /**
+ * Report the correct tightly bounded dimensions of the view.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(VIEW_DIM_X, VIEW_DIM_Y);
+ }
+
+ /**
+ * Wrap Math.round(). I'm not a Java expert. Is this the only way to avoid writing "(int)Math.round" everywhere?
+ * @param x
+ * @return
+ */
+ private int round(double x) {
+ return (int)Math.round(x);
+ }
+
+ /**
+ * Limit a value to the range [0,1].
+ * @param n
+ * @return
+ */
+ private float pinToUnit(float n) {
+ if (n < 0) {
+ n = 0;
+ } else if (n > 1) {
+ n = 1;
+ }
+ return n;
+ }
+
+ /**
+ * Limit a value to the range [0,max].
+ * @param n
+ * @param max
+ * @return
+ */
+ private float pin(float n, float max) {
+ if (n < 0) {
+ n = 0;
+ } else if (n > max) {
+ n = max;
+ }
+ return n;
+ }
+
+ /**
+ * Limit a value to the range [min,max].
+ * @param n
+ * @param min
+ * @param max
+ * @return
+ */
+ private float pin(float n, float min, float max) {
+ if (n < min) {
+ n = min;
+ } else if (n > max) {
+ n = max;
+ }
+ return n;
+ }
+
+ /**
+ * No clue what this does (some sort of average/mean I presume). It came with the original UberColorPickerDialog
+ * in the API Demos and wasn't documented. I don't feel like spending any time figuring it out, I haven't looked at it at all.
+ * @param s
+ * @param d
+ * @param p
+ * @return
+ */
+ private int ave(int s, int d, float p) {
+ return s + round(p * (d - s));
+ }
+
+ /**
+ * Came with the original UberColorPickerDialog in the API Demos, wasn't documented. I believe it takes an array of
+ * colors and a value in the range [0,1] and interpolates a resulting color in a seemingly predictable manner.
+ * I haven't looked at it at all.
+ * @param colors
+ * @param unit
+ * @return
+ */
+ private int interpColor(int colors[], float unit) {
+ if (unit <= 0) {
+ return colors[0];
+ }
+ if (unit >= 1) {
+ return colors[colors.length - 1];
+ }
+
+ float p = unit * (colors.length - 1);
+ int i = (int)p;
+ p -= i;
+
+ // now p is just the fractional part [0...1) and i is the index
+ int c0 = colors[i];
+ int c1 = colors[i+1];
+ int a = ave(Color.alpha(c0), Color.alpha(c1), p);
+ int r = ave(Color.red(c0), Color.red(c1), p);
+ int g = ave(Color.green(c0), Color.green(c1), p);
+ int b = ave(Color.blue(c0), Color.blue(c1), p);
+
+ return Color.argb(a, r, g, b);
+ }
+
+ /**
+ * A standard point-in-rect routine.
+ * @param x
+ * @param y
+ * @param r
+ * @return true if point x,y is in rect r
+ */
+ public boolean ptInRect(int x, int y, Rect r) {
+ return x > r.left && x < r.right && y > r.top && y < r.bottom;
+ }
+
+ /**
+ * Process trackball events. Used mainly for fine-tuned color adjustment, or alternatively to switch between slider controls.
+ */
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ //A longer event history implies faster trackball movement.
+ //Use it to infer a larger jump and therefore faster palette/slider adjustment.
+ int jump = event.getHistorySize() + 1;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ }
+ break;
+ case MotionEvent.ACTION_MOVE: {
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the appropriate entry in this list,
+ //depending on whether you use 1D or 2D controllers
+ switch (mMethod) {
+ case METHOD_HS_V_PALETTE:
+ if (mFocusedControl == 0) {
+ changeHSPalette(x, y, jump);
+ }
+ else if (mFocusedControl == 1) {
+ if (y < 0)
+ changeSlider(mFocusedControl, true, jump);
+ else if (y > 0)
+ changeSlider(mFocusedControl, false, jump);
+ }
+ break;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP: {
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the appropriate functions below,
+ //one per UI controller in the new method
+ /**
+ * Effect a trackball change to a 2D palette.
+ * @param x -1: negative x change, 0: no x change, +1: positive x change.
+ * @param y -1: negative y change, 0, no y change, +1: positive y change.
+ * @param jump the amount by which to change.
+ */
+ private void changeHSPalette(float x, float y, int jump) {
+ int x2 = 0, y2 = 0;
+ if (x < 0)
+ x2 = -jump;
+ else if (x > 0)
+ x2 = jump;
+ if (y < 0)
+ y2 = -jump;
+ else if (y > 0)
+ y2 = jump;
+
+ mCoord[0] += x2;
+ mCoord[1] += y2;
+
+ if (mCoord[0] < -PALETTE_RADIUS)
+ mCoord[0] = -PALETTE_RADIUS;
+ else if (mCoord[0] > PALETTE_RADIUS)
+ mCoord[0] = PALETTE_RADIUS;
+ if (mCoord[1] < -PALETTE_RADIUS)
+ mCoord[1] = -PALETTE_RADIUS;
+ else if (mCoord[1] > PALETTE_RADIUS)
+ mCoord[1] = PALETTE_RADIUS;
+
+ float radius = (float)java.lang.Math.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]);
+ if (radius > PALETTE_RADIUS)
+ radius = PALETTE_RADIUS;
+
+ float angle = (float)java.lang.Math.atan2(mCoord[1], mCoord[0]);
+ // need to turn angle [-PI ... PI] into unit [0....1]
+ float unit = angle/(2*PI);
+ if (unit < 0) {
+ unit += 1;
+ }
+
+ mCoord[0] = round(Math.cos(angle) * radius);
+ mCoord[1] = round(Math.sin(angle) * radius);
+
+ int c = interpColor(mSpectrumColorsRev, unit);
+ float[] hsv = new float[3];
+ Color.colorToHSV(c, hsv);
+ mHSV[0] = hsv[0];
+ mHSV[1] = radius / PALETTE_RADIUS;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setVerValSlider();
+
+ invalidate();
+ }
+
+ /**
+ * Effect a trackball change to a 1D slider.
+ * @param slider id of the slider to be effected
+ * @param increase true if the change is an increase, false if a decrease
+ * @param jump the amount by which to change in units of the range [0,255]
+ */
+ private void changeSlider(int slider, boolean increase, int jump) {
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //It is only necessary to add an entry here for a new method if the new method uses a 1D slider.
+ //Note, some sliders are horizontal and others are vertical.
+ //They differ a bit, especially in a sign flip on the vertical axis.
+ if (mMethod == METHOD_HS_V_PALETTE) {
+ //slider *must* equal 1
+
+ mHSV[2] += (increase ? jump : -jump) / 256.0f;
+ mHSV[2] = pinToUnit(mHSV[2]);
+ updateAllFromHSV();
+ mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setOvalValDimmer();
+
+ invalidate();
+ }
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateRGBfromHSV() {
+ int color = Color.HSVToColor(mHSV);
+ mRGB[0] = Color.red(color);
+ mRGB[1] = Color.green(color);
+ mRGB[2] = Color.blue(color);
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateYUVfromRGB() {
+ float r = mRGB[0] / 255.0f;
+ float g = mRGB[1] / 255.0f;
+ float b = mRGB[2] / 255.0f;
+
+ ColorMatrix cm = new ColorMatrix();
+ cm.setRGB2YUV();
+ final float[] a = cm.getArray();
+
+ mYUV[0] = a[0] * r + a[1] * g + a[2] * b;
+ mYUV[0] = pinToUnit(mYUV[0]);
+ mYUV[1] = a[5] * r + a[6] * g + a[7] * b;
+ mYUV[1] = pin(mYUV[1], -.5f, .5f);
+ mYUV[2] = a[10] * r + a[11] * g + a[12] * b;
+ mYUV[2] = pin(mYUV[2], -.5f, .5f);
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateHexFromHSV() {
+ //For now, assume 100% opacity
+ mHexStr = Integer.toHexString(Color.HSVToColor(mHSV)).toUpperCase();
+ mHexStr = mHexStr.substring(2, mHexStr.length());
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateAllFromHSV() {
+ //Update mRGB
+ if (mRGBenabled || mYUVenabled)
+ updateRGBfromHSV();
+
+ //Update mYUV
+ if (mYUVenabled)
+ updateYUVfromRGB();
+
+ //Update mHexStr
+ if (mRGBenabled)
+ updateHexFromHSV();
+ }
+
+ /**
+ * Process touch events: down, move, and up
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ //Generate coordinates which are palette=local with the origin at the upper left of the main 2D palette
+ int y2 = (int)(pin(round(y - PALETTE_POS_Y), PALETTE_DIM));
+
+ //Generate coordinates which are palette-local with the origin at the center of the main 2D palette
+ float circlePinnedX = x - PALETTE_POS_X - PALETTE_CENTER_X;
+ float circlePinnedY = y - PALETTE_POS_Y - PALETTE_CENTER_Y;
+
+ //Is the event in a swatch?
+ boolean inSwatchOld = ptInRect(round(x), round(y), mOldSwatchRect);
+ boolean inSwatchNew = ptInRect(round(x), round(y), mNewSwatchRect);
+
+ //Get the event's distance from the center of the main 2D palette
+ float radius = (float)java.lang.Math.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY);
+
+ //Is the event in a circle-pinned 2D palette?
+ boolean inOvalPalette = radius <= PALETTE_RADIUS;
+
+ //Pin the radius
+ if (radius > PALETTE_RADIUS)
+ radius = PALETTE_RADIUS;
+
+ //Is the event in a vertical slider to the right of the main 2D palette
+ boolean inVerSlider = ptInRect(round(x), round(y), mVerSliderRect);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTracking = TRACKED_NONE;
+
+ if (inSwatchOld)
+ mTracking = TRACK_SWATCH_OLD;
+ else if (inSwatchNew)
+ mTracking = TRACK_SWATCH_NEW;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last entry in this list
+ else if (mMethod == METHOD_HS_V_PALETTE) {
+ if (inOvalPalette) {
+ mTracking = TRACK_HS_PALETTE;
+ mFocusedControl = 0;
+ }
+ else if (inVerSlider) {
+ mTracking = TRACK_VER_VALUE_SLIDER;
+ mFocusedControl = 1;
+ }
+ }
+ case MotionEvent.ACTION_MOVE:
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the entries in this list,
+ //one per UI controller the new method requires.
+ if (mTracking == TRACK_HS_PALETTE) {
+ float angle = (float)java.lang.Math.atan2(circlePinnedY, circlePinnedX);
+ // need to turn angle [-PI ... PI] into unit [0....1]
+ float unit = angle/(2*PI);
+ if (unit < 0) {
+ unit += 1;
+ }
+
+ mCoord[0] = round(Math.cos(angle) * radius);
+ mCoord[1] = round(Math.sin(angle) * radius);
+
+ int c = interpColor(mSpectrumColorsRev, unit);
+ float[] hsv = new float[3];
+ Color.colorToHSV(c, hsv);
+ mHSV[0] = hsv[0];
+ mHSV[1] = radius / PALETTE_RADIUS;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setVerValSlider();
+
+ invalidate();
+ }
+ else if (mTracking == TRACK_VER_VALUE_SLIDER) {
+ if (mCoord[2] != y2) {
+ mCoord[2] = y2;
+ float value = 1.0f - (float)y2 / (float)PALETTE_DIM;
+
+ mHSV[2] = value;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setOvalValDimmer();
+
+ invalidate();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last entry in this list.
+ if (mTracking == TRACK_SWATCH_OLD && inSwatchOld) {
+ Color.colorToHSV(mOriginalColor, mHSV);
+ mSwatchNew.setColor(mOriginalColor);
+ initUI();
+ invalidate();
+ }
+ else if (mTracking == TRACK_SWATCH_NEW && inSwatchNew) {
+ mListener.colorChanged(mSwatchNew.getColor());
+ invalidate();
+ }
+
+ mTracking= TRACKED_NONE;
+ break;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/VolumePreference.java b/app/src/main/java/org/connectbot/util/VolumePreference.java
new file mode 100644
index 0000000..2e7f61c
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/VolumePreference.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+/**
+ * @author kenny
+ *
+ */
+public class VolumePreference extends DialogPreference implements OnSeekBarChangeListener {
+ /**
+ * @param context
+ * @param attrs
+ */
+ public VolumePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setupLayout(context, attrs);
+ }
+
+ public VolumePreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setupLayout(context, attrs);
+ }
+
+ private void setupLayout(Context context, AttributeSet attrs) {
+ setPersistent(true);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ SeekBar sb = new SeekBar(getContext());
+
+ sb.setMax(100);
+ sb.setProgress((int)(getPersistedFloat(
+ PreferenceConstants.DEFAULT_BELL_VOLUME) * 100));
+ sb.setPadding(10, 10, 10, 10);
+ sb.setOnSeekBarChangeListener(this);
+
+ return sb;
+ }
+
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ persistFloat(progress / 100f);
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) { }
+
+ public void onStopTrackingTouch(SeekBar seekBar) { }
+}
diff --git a/app/src/main/java/org/connectbot/util/XmlBuilder.java b/app/src/main/java/org/connectbot/util/XmlBuilder.java
new file mode 100644
index 0000000..4a6f62d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/XmlBuilder.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util;
+
+import com.trilead.ssh2.crypto.Base64;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class XmlBuilder {
+ private StringBuilder sb;
+
+ public XmlBuilder() {
+ sb = new StringBuilder();
+ }
+
+ public XmlBuilder append(String data) {
+ sb.append(data);
+
+ return this;
+ }
+
+ public XmlBuilder append(String field, Object data) {
+ if (data == null) {
+ sb.append(String.format("<%s/>", field));
+ } else if (data instanceof String) {
+ String input = (String) data;
+ boolean binary = false;
+
+ for (byte b : input.getBytes()) {
+ if (b < 0x20 || b > 0x7e) {
+ binary = true;
+ break;
+ }
+ }
+
+ sb.append(String.format("<%s>%s</%s>", field,
+ binary ? new String(Base64.encode(input.getBytes())) : input, field));
+ } else if (data instanceof Integer) {
+ sb.append(String.format("<%s>%d</%s>", field, (Integer) data, field));
+ } else if (data instanceof Long) {
+ sb.append(String.format("<%s>%d</%s>", field, (Long) data, field));
+ } else if (data instanceof byte[]) {
+ sb.append(String.format("<%s>%s</%s>", field, new String(Base64.encode((byte[]) data)), field));
+ } else if (data instanceof Boolean) {
+ sb.append(String.format("<%s>%s</%s>", field, (Boolean) data, field));
+ }
+
+ return this;
+ }
+
+ public String toString() {
+ return sb.toString();
+ }
+}
diff --git a/app/src/main/java/org/keyczar/jce/EcCore.java b/app/src/main/java/org/keyczar/jce/EcCore.java
new file mode 100644
index 0000000..681d5db
--- /dev/null
+++ b/app/src/main/java/org/keyczar/jce/EcCore.java
@@ -0,0 +1,679 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * 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.keyczar.jce;
+
+import java.math.BigInteger;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+
+/**
+ * This class implements the basic EC operations such as point addition and
+ * doubling and point multiplication. Only NSA Suite B / NIST curves are
+ * supported.
+ *
+ * Todo:
+ * - Add (more) comments - Performance optimizations - Cleanup ASN.1 code,
+ * possibly replace with own impl - ...
+ *
+ * References:
+ *
+ * [1] Software Implementation of the NIST Elliptic Curves Over Prime Fields, M.
+ * Brown et al. [2] Efficient elliptic curve exponentiation using mixed
+ * coordinates, H. Cohen et al. [3] SEC 1: Elliptic Curve Cryptography. [4]
+ * Guide to Elliptic Curve Cryptography, D. Hankerson et al., Springer.
+ *
+ * @author martclau@gmail.com
+ *
+ */
+// BEGIN connectbot-changed
+public final class EcCore {
+// END connectbot-changed
+// BEGIN connectbot-removed
+// private static final long serialVersionUID = -1376116429660095993L;
+//
+// private static final String INFO = "Google Keyczar (EC key/parameter generation; EC signing)";
+//
+// public static final String NAME = "GooKey";
+//
+// @SuppressWarnings("unchecked")
+// public EcCore() {
+// super(NAME, 0.1, INFO);
+// AccessController.doPrivileged(new PrivilegedAction<Object>() {
+// @Override
+// public Object run() {
+// put("Signature.SHA1withECDSA", "org.keyczar.jce.EcSignatureImpl$SHA1");
+// put("Alg.Alias.Signature.ECDSA", "SHA1withDSA");
+// put("Signature.SHA256withECDSA",
+// "org.keyczar.jce.EcSignatureImpl$SHA256");
+// put("Signature.SHA384withECDSA",
+// "org.keyczar.jce.EcSignatureImpl$SHA384");
+// put("Signature.SHA512withECDSA",
+// "org.keyczar.jce.EcSignatureImpl$SHA512");
+// put("KeyPairGenerator.EC", "org.keyczar.jce.EcKeyPairGeneratorImpl");
+// put("KeyFactory.EC", "org.keyczar.jce.EcKeyFactoryImpl");
+// put("Signature.SHA1withECDSA KeySize", "521");
+// put("Signature.SHA1withECDSA ImplementedIn", "Software");
+// put("Signature.SHA256withECDSA KeySize", "521");
+// put("Signature.SHA256withECDSA ImplementedIn", "Software");
+// put("Signature.SHA384withECDSA KeySize", "521");
+// put("Signature.SHA384withECDSA ImplementedIn", "Software");
+// put("Signature.SHA512withECDSA KeySize", "521");
+// put("Signature.SHA512withECDSA ImplementedIn", "Software");
+// put("KeyPairGenerator.EC ImplementedIn", "Software");
+// put("KeyFactory.EC ImplementedIn", "Software");
+// return null;
+// }
+// });
+// }
+//
+// private static final ECParameterSpec P192 = new ECParameterSpec(
+// new EllipticCurve(
+// new ECFieldFp(new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF", 16)),
+// new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC", 16),
+// new BigInteger("64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1", 16)),
+// new ECPoint(
+// new BigInteger("188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012", 16),
+// new BigInteger("07192B95FFC8DA78631011ED6B24CDD573F977A11E794811", 16)),
+// new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831", 16), 1);
+//
+// private static final ECParameterSpec P224 = new ECParameterSpec(
+// new EllipticCurve(new ECFieldFp(new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001", 16)),
+// new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE", 16),
+// new BigInteger(
+// "B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4", 16)),
+// new ECPoint(new BigInteger(
+// "B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21", 16),
+// new BigInteger(
+// "BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34", 16)),
+// new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D", 16), 1);
+//
+// private static final ECParameterSpec P256 = new ECParameterSpec(
+// new EllipticCurve(new ECFieldFp(new BigInteger(
+// "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
+// 16)), new BigInteger(
+// "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
+// 16), new BigInteger(
+// "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B",
+// 16)), new ECPoint(new BigInteger(
+// "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
+// 16), new BigInteger(
+// "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
+// 16)), new BigInteger(
+// "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551",
+// 16), 1);
+//
+// private static final ECParameterSpec P384 = new ECParameterSpec(
+// new EllipticCurve(
+// new ECFieldFp(
+// new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF",
+// 16)),
+// new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC",
+// 16),
+// new BigInteger(
+// "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF",
+// 16)),
+// new ECPoint(
+// new BigInteger(
+// "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7",
+// 16),
+// new BigInteger(
+// "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F",
+// 16)),
+// new BigInteger(
+// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973",
+// 16), 1);
+//
+// private static final ECParameterSpec P521 = new ECParameterSpec(
+// new EllipticCurve(
+// new ECFieldFp(
+// new BigInteger(
+// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+// 16)),
+// new BigInteger(
+// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC",
+// 16),
+// new BigInteger(
+// "0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00",
+// 16)),
+// new ECPoint(
+// new BigInteger(
+// "00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66",
+// 16),
+// new BigInteger(
+// "011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650",
+// 16)),
+// new BigInteger(
+// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409",
+// 16), 1);
+//
+// public static final String EC_PARAMS_P192_OID = "1.2.840.10045.3.1.1";
+// public static final String EC_PARAMS_P224_OID = "1.3.132.0.33";
+// public static final String EC_PARAMS_P256_OID = "1.2.840.10045.3.1.7";
+// public static final String EC_PARAMS_P384_OID = "1.3.132.0.34";
+// public static final String EC_PARAMS_P521_OID = "1.3.132.0.35";
+//
+// private static Map<String, ECParameterSpec> oidMap = new HashMap<String, ECParameterSpec>();
+// private static Map<ECParameterSpec, String> paramsMap = new HashMap<ECParameterSpec, String>();
+// private static Map<ECParameterSpec, String> friendlyNameMap = new HashMap<ECParameterSpec, String>();
+//
+// static {
+// oidMap.put(EC_PARAMS_P192_OID, P192);
+// oidMap.put(EC_PARAMS_P224_OID, P224);
+// oidMap.put(EC_PARAMS_P256_OID, P256);
+// oidMap.put(EC_PARAMS_P384_OID, P384);
+// oidMap.put(EC_PARAMS_P521_OID, P521);
+// paramsMap.put(P192, EC_PARAMS_P192_OID);
+// paramsMap.put(P224, EC_PARAMS_P224_OID);
+// paramsMap.put(P256, EC_PARAMS_P256_OID);
+// paramsMap.put(P384, EC_PARAMS_P384_OID);
+// paramsMap.put(P521, EC_PARAMS_P521_OID);
+// friendlyNameMap.put(P192, "P-192");
+// friendlyNameMap.put(P224, "P-224");
+// friendlyNameMap.put(P256, "P-256");
+// friendlyNameMap.put(P384, "P-384");
+// friendlyNameMap.put(P521, "P-521");
+// }
+//
+// public static ECParameterSpec getParams(String oid) {
+// ECParameterSpec params;
+// if ((params = oidMap.get(oid)) != null) return params;
+// throw new IllegalArgumentException("Unsupported EC parameters: " + oid);
+// }
+//
+// public static String getOID(ECParameterSpec params) {
+// String oid;
+// if ((oid = paramsMap.get(params)) != null) return oid;
+// throw new IllegalArgumentException("Unsupport EC parameters");
+// }
+//
+// public static String getFriendlyName(ECParameterSpec params) {
+// String name;
+// if ((name = friendlyNameMap.get(params)) != null) return name;
+// throw new IllegalArgumentException("Unsupport EC parameters");
+// }
+//
+// private static final BigInteger ZERO = BigInteger.ZERO;
+// private static final BigInteger ONE = BigInteger.ONE;
+// private static final BigInteger TWO = BigInteger.valueOf(2);
+// END connectbot-removed
+ private static final BigInteger THREE = BigInteger.valueOf(3);
+// BEGIN connectbot-removed
+// private static final BigInteger FOUR = BigInteger.valueOf(4);
+// private static final BigInteger EIGHT = BigInteger.valueOf(8);
+// END connectbot-removed
+
+ private static BigInteger[] doublePointA(BigInteger[] P,
+ ECParameterSpec params) {
+ final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+ final BigInteger a = params.getCurve().getA();
+
+ if (P[0] == null || P[1] == null) return P;
+
+ BigInteger d = (P[0].pow(2).multiply(THREE).add(a)).multiply(P[1]
+ .shiftLeft(1).modInverse(p));
+ BigInteger[] R = new BigInteger[2];
+ R[0] = d.pow(2).subtract(P[0].shiftLeft(1)).mod(p);
+ R[1] = d.multiply(P[0].subtract(R[0])).subtract(P[1]).mod(p);
+
+ return R;
+ }
+
+ private static BigInteger[] addPointsA(BigInteger[] P1, BigInteger[] P2,
+ ECParameterSpec params) {
+ final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+
+ if (P2[0] == null || P2[1] == null) return P1;
+
+ if (P1[0] == null || P1[1] == null) return P2;
+
+ BigInteger d = (P2[1].subtract(P1[1])).multiply((P2[0].subtract(P1[0]))
+ .modInverse(p));
+ BigInteger[] R = new BigInteger[2];
+ R[0] = d.pow(2).subtract(P1[0]).subtract(P2[0]).mod(p);
+ R[1] = d.multiply(P1[0].subtract(R[0])).subtract(P1[1]).mod(p);
+
+ return R;
+ }
+
+ public static BigInteger[] multiplyPointA(BigInteger[] P, BigInteger k,
+ ECParameterSpec params) {
+ BigInteger[] Q = new BigInteger[] {null, null};
+
+ for (int i = k.bitLength() - 1; i >= 0; i--) {
+ Q = doublePointA(Q, params);
+ if (k.testBit(i)) Q = addPointsA(Q, P, params);
+ }
+
+ return Q;
+ }
+
+// BEGIN connectbot-removed
+// private static BigInteger[] doublePointJ(BigInteger[] P,
+// ECParameterSpec params) {
+// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+// BigInteger A, B, C, D;
+//
+// if (P[2].signum() == 0) // point at inf
+// return P;
+//
+// A = FOUR.multiply(P[0]).multiply(P[1].pow(2)).mod(p);
+// B = EIGHT.multiply(P[1].pow(4)).mod(p);
+// C = THREE.multiply(P[0].subtract(P[2].pow(2))).multiply(
+// P[0].add(P[2].pow(2))).mod(p);
+// D = C.pow(2).subtract(A.add(A)).mod(p);
+//
+// return new BigInteger[] {
+// D, C.multiply(A.subtract(D)).subtract(B).mod(p),
+// TWO.multiply(P[1]).multiply(P[2]).mod(p)};
+// }
+//
+// private static BigInteger[] addPointsJA(BigInteger[] P1, BigInteger[] P2,
+// ECParameterSpec params) {
+// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+// BigInteger A, B, C, D;
+// BigInteger X3;
+//
+// if (P1[2].signum() == 0) // point at inf
+// return new BigInteger[] {P2[0], P2[1], ONE};
+//
+// A = P2[0].multiply(P1[2].pow(2)).mod(p);
+// B = P2[1].multiply(P1[2].pow(3)).mod(p);
+// C = A.subtract(P1[0]).mod(p);
+// D = B.subtract(P1[1]).mod(p);
+//
+// X3 = D.pow(2)
+// .subtract(C.pow(3).add(TWO.multiply(P1[0]).multiply(C.pow(2)))).mod(p);
+// return new BigInteger[] {
+// X3,
+// D.multiply(P1[0].multiply(C.pow(2)).subtract(X3)).subtract(
+// P1[1].multiply(C.pow(3))).mod(p), P1[2].multiply(C).mod(p)};
+// }
+//
+// // Binary NAF method for point multiplication
+// public static BigInteger[] multiplyPoint(BigInteger[] P, BigInteger k,
+// ECParameterSpec params) {
+// BigInteger h = THREE.multiply(k);
+//
+// BigInteger[] Pneg = new BigInteger[] {P[0], P[1].negate()};
+// BigInteger[] R = new BigInteger[] {P[0], P[1], ONE};
+//
+// int bitLen = h.bitLength();
+// for (int i = bitLen - 2; i > 0; --i) {
+// R = doublePointJ(R, params);
+// if (h.testBit(i)) R = addPointsJA(R, P, params);
+// if (k.testBit(i)) R = addPointsJA(R, Pneg, params);
+// }
+//
+// // // <DEBUG>
+// // BigInteger[] SS = new BigInteger[] { R[0], R[1], R[2] };
+// // toAffine(SS, params);
+// // BigInteger[] RR = multiplyPointA(P, k, params);
+// // if (!SS[0].equals(RR[0]) || !SS[1].equals(RR[1]))
+// // throw new RuntimeException("Internal mult error");
+// // // </DEBUG>
+//
+// return R;
+// }
+
+// // Simultaneous multiple point multiplication, also known as Shamir's trick
+// static BigInteger[] multiplyPoints(BigInteger[] P, BigInteger k,
+// BigInteger[] Q, BigInteger l, ECParameterSpec params) {
+// BigInteger[] PQ = addPointsA(P, Q, params);
+// BigInteger[] R = new BigInteger[] {null, null, ZERO};
+//
+// int max = Math.max(k.bitLength(), l.bitLength());
+// for (int i = max - 1; i >= 0; --i) {
+// R = doublePointJ(R, params);
+// if (k.testBit(i)) {
+// if (l.testBit(i))
+// R = addPointsJA(R, PQ, params);
+// else
+// R = addPointsJA(R, P, params);
+// } else if (l.testBit(i)) R = addPointsJA(R, Q, params);
+// }
+//
+// // // <DEBUG>
+// // BigInteger[] SS = new BigInteger[] { R[0], R[1], R[2] };
+// // toAffine(SS, params);
+// // BigInteger[] AA = multiplyPointA(P, k, params);
+// // BigInteger[] BB = multiplyPointA(Q, l, params);
+// // BigInteger[] AB = addPointsA(AA, BB, params);
+// // if (!SS[0].equals(AB[0]) || !SS[1].equals(AB[1]))
+// // throw new RuntimeException("Internal mult error");
+// // // </DEBUG>
+//
+// return R;
+// }
+//
+// // SEC 1, 2.3.5
+// static byte[] fieldElemToBytes(BigInteger a, ECParameterSpec params) {
+// int len = (((ECFieldFp) params.getCurve().getField()).getP().bitLength() + 7) / 8;
+// byte[] bytes = a.toByteArray();
+// if (len < bytes.length) {
+// byte[] tmp = new byte[len];
+// System.arraycopy(bytes, bytes.length - tmp.length, tmp, 0, tmp.length);
+// return tmp;
+// } else if (len > bytes.length) {
+// byte[] tmp = new byte[len];
+// System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length);
+// return tmp;
+// }
+// return bytes;
+// }
+//
+// static int fieldElemToBytes(BigInteger a, ECParameterSpec params,
+// byte[] data, int off) {
+// int len = (((ECFieldFp) params.getCurve().getField()).getP().bitLength() + 7) / 8;
+// byte[] bytes = a.toByteArray();
+// if (len < bytes.length) {
+// System.arraycopy(bytes, bytes.length - len, data, off, len);
+// return len;
+// } else if (len > bytes.length) {
+// System.arraycopy(bytes, 0, data, len - bytes.length + off, bytes.length);
+// return len;
+// }
+// System.arraycopy(bytes, 0, data, off, bytes.length);
+// return bytes.length;
+// }
+//
+// // SEC 1, 2.3.3
+// static byte[] ecPointToBytes(ECPoint a, ECParameterSpec params) {
+// byte[] fe1 = fieldElemToBytes(a.getAffineX(), params);
+// byte[] fe2 = fieldElemToBytes(a.getAffineY(), params);
+// byte[] bytes = new byte[1 + fe1.length + fe2.length];
+// bytes[0] = 0x04;
+// System.arraycopy(fe1, 0, bytes, 1, fe1.length);
+// System.arraycopy(fe2, 0, bytes, 1 + fe1.length, fe2.length);
+// return bytes;
+// }
+//
+// // SEC 1, 2.3.4
+// static ECPoint bytesToECPoint(byte[] bytes, ECParameterSpec params) {
+// switch (bytes[0]) {
+// case 0x00: // point at inf
+// throw new IllegalArgumentException(
+// "Point at infinity is not a valid argument");
+// case 0x02: // point compression
+// case 0x03:
+// throw new UnsupportedOperationException(
+// "Point compression is not supported");
+// case 0x04:
+// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+// byte[] fe = new byte[(p.bitLength() + 7) / 8];
+// System.arraycopy(bytes, 1, fe, 0, fe.length);
+// BigInteger x = new BigInteger(1, fe);
+// System.arraycopy(bytes, 1 + fe.length, fe, 0, fe.length);
+// return new ECPoint(x, new BigInteger(1, fe));
+// default:
+// throw new IllegalArgumentException("Invalid point encoding");
+// }
+// }
+//
+// // Convert Jacobian point to affine
+// static void toAffine(BigInteger[] P, ECParameterSpec params) {
+// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+// P[0] = P[0].multiply(P[2].pow(2).modInverse(p)).mod(p);
+// P[1] = P[1].multiply(P[2].pow(3).modInverse(p)).mod(p);
+// }
+//
+// static void toAffineX(BigInteger[] P, ECParameterSpec params) {
+// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP();
+// P[0] = P[0].multiply(P[2].pow(2).modInverse(p)).mod(p);
+// }
+//
+// static BigInteger[] internalPoint(ECPoint P) {
+// return new BigInteger[] {P.getAffineX(), P.getAffineY()};
+// }
+//
+// // private static void printPerf(String msg, long start, long stop) {
+// // String unit = "ms";
+// // long diff = stop - start;
+// // if (diff > 1000) {
+// // diff /= 1000;
+// // unit = "s";
+// // }
+// // System.out.printf("%s: %d %s\n", msg, diff, unit);
+// // }
+//
+// public static void main(String[] args) throws Exception {
+//
+// Security.insertProviderAt(new EcCore(), 0);
+//
+// // ----
+// // Test primitives
+// // ----
+//
+// // GooKey EC private key, 256 bit
+// // Private value:
+// // a9231e0d113abdacd3bb5edb24124fbef6f562c5f90b835670f5e48f775019f2
+// // Parameters: P-256 (1.2.840.10045.3.1.7)
+// // GooKey EC public key, 256 bit
+// // Public value (x coordinate):
+// // 86645e0320c0f9dc1a9b8456396cc105754df67a9829c21e13ab6ecf944cf68c
+// // Public value (y coordinate):
+// // ea1721a578043d48f12738359b5eb5f0dac2242ec6128ee0ab6ff40c8fe0cae6
+// // Parameters: P-256 (1.2.840.10045.3.1.7)
+// // GooKey EC private key, 256 bit
+// // Private value:
+// // b84d5cfab214fc3928864abb85f668a85b1006ca0147c78f22deb1dcc7e4a022
+// // Parameters: P-256 (1.2.840.10045.3.1.7)
+// // GooKey EC public key, 256 bit
+// // Public value (x coordinate):
+// // 61f6f7264f0a19f0debcca3efd079667a0112cc0b8be07a815b4c375e96ad3d1
+// // Public value (y coordinate):
+// // 3308c0016d776ed5aa9f021e43348b2e684b3b7a0f25dc9e4c8670b5d87cb705
+// // Parameters: P-256 (1.2.840.10045.3.1.7)
+//
+// // P = kG
+// BigInteger k = new BigInteger(
+// "a9231e0d113abdacd3bb5edb24124fbef6f562c5f90b835670f5e48f775019f2", 16);
+// BigInteger[] P = new BigInteger[] {
+// new BigInteger(
+// "86645e0320c0f9dc1a9b8456396cc105754df67a9829c21e13ab6ecf944cf68c",
+// 16),
+// new BigInteger(
+// "ea1721a578043d48f12738359b5eb5f0dac2242ec6128ee0ab6ff40c8fe0cae6",
+// 16), ONE};
+//
+// // Q = lG
+// BigInteger l = new BigInteger(
+// "b84d5cfab214fc3928864abb85f668a85b1006ca0147c78f22deb1dcc7e4a022", 16);
+// BigInteger[] Q = new BigInteger[] {
+// new BigInteger(
+// "61f6f7264f0a19f0debcca3efd079667a0112cc0b8be07a815b4c375e96ad3d1",
+// 16),
+// new BigInteger(
+// "3308c0016d776ed5aa9f021e43348b2e684b3b7a0f25dc9e4c8670b5d87cb705",
+// 16), ONE};
+//
+// // Known answer for P+Q
+// BigInteger[] kat1 = new BigInteger[] {
+// new BigInteger(
+// "bc7adb05bca2460bbfeb4e0f88b61c384ea88ed3fd56017938ac2582513d4220",
+// 16),
+// new BigInteger(
+// "a640a43df2e9df39eec11445b7e3f7835b743ef1ac4a83cecb570a060b3f1c6c",
+// 16)};
+//
+// BigInteger[] R = addPointsA(P, Q, P256);
+// if (!R[0].equals(kat1[0]) || !R[1].equals(kat1[1]))
+// throw new RuntimeException("kat1 failed");
+//
+// R = addPointsJA(P, Q, P256);
+// toAffine(R, P256);
+// if (!R[0].equals(kat1[0]) || !R[1].equals(kat1[1]))
+// throw new RuntimeException("kat1 failed");
+//
+//
+// // Known answer for Q+Q
+// BigInteger[] kat2 = new BigInteger[] {
+// new BigInteger(
+// "c79d7f9100c14a70f0bb9bdce59654abf99e10d1ac5afc1a0f1b6bc650d6429b",
+// 16),
+// new BigInteger(
+// "6856814e47adce42bc0d7c3bef308c6c737c418ed093effb31e21f53c7735c97",
+// 16)};
+//
+// R = doublePointA(P, P256);
+// if (!R[0].equals(kat2[0]) || !R[1].equals(kat2[1]))
+// throw new RuntimeException("kat2 failed");
+//
+// R = doublePointJ(P, P256);
+// toAffine(R, P256);
+// if (!R[0].equals(kat2[0]) || !R[1].equals(kat2[1]))
+// throw new RuntimeException("kat2 failed");
+//
+// // Known answer for kP
+// BigInteger[] kat3 = new BigInteger[] {
+// new BigInteger(
+// "97a82a834b9e6b50660ae30d43dac9b200276e8bcd2ed6a6593048de09276d1a",
+// 16),
+// new BigInteger(
+// "30a9590a01066d8ef54a910afcc8648dbc7400c01750af423ce95547f2154d56",
+// 16)};
+//
+// R = multiplyPointA(P, k, P256);
+// if (!R[0].equals(kat3[0]) || !R[1].equals(kat3[1]))
+// throw new RuntimeException("kat3 failed");
+//
+// R = multiplyPoint(P, k, P256);
+// toAffine(R, P256);
+// if (!R[0].equals(kat3[0]) || !R[1].equals(kat3[1]))
+// throw new RuntimeException("kat3 failed");
+//
+// // Known answer for kP+lQ
+// BigInteger[] kat4 = new BigInteger[] {
+// new BigInteger(
+// "6fd51be5cf3d6a6bcb62594bbe41ccf549b37d8fefff6e293a5bea0836efcfc6",
+// 16),
+// new BigInteger(
+// "9bc21a930137aa3814908974c431e4545a05dce61321253c337f3883129c42ca",
+// 16)};
+//
+// BigInteger[] RR = multiplyPointA(Q, l, P256);
+// R = addPointsA(R, RR, P256);
+// if (!R[0].equals(kat4[0]) || !R[1].equals(kat4[1]))
+// throw new RuntimeException("kat4 failed");
+//
+// R = multiplyPoints(P, k, Q, l, P256);
+// toAffine(R, P256);
+// if (!R[0].equals(kat4[0]) || !R[1].equals(kat4[1]))
+// throw new RuntimeException("kat4 failed");
+//
+// // ----
+// // Test ECDSA in various combinations
+// // ----
+//
+// Provider gooProv = Security.getProvider("GooKey");
+// Provider nssProv = Security.getProvider("SunPKCS11-NSS");
+//
+// // Number of iterations: trust me, this is a (stress) good test
+// // and does provoke bugs in a fuzzing way.
+// int iter = 50;
+//
+// // Iterate over all key lengths and signature schemes.
+// int[] keyLengths = {192, 224, 256, 384, 521};
+// String[] ecdsas = {
+// "SHA1withECDSA", "SHA256withECDSA", "SHA384withECDSA",
+// "SHA512withECDSA"};
+// for (int s = 0; s < ecdsas.length; s++) {
+// System.out.println("Signature scheme " + ecdsas[s]);
+// for (int i = 0; i < keyLengths.length; i++) {
+// System.out.print("Testing P-" + keyLengths[i] + ": ");
+// for (int n = 0; n < iter; n++) {
+// System.out.print(".");
+//
+// KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", gooProv);
+// kpGen.initialize(keyLengths[i]);
+// KeyPair ecKeyPair = kpGen.generateKeyPair();
+//
+// ECPrivateKey ecPrivKey = (ECPrivateKey) ecKeyPair.getPrivate();
+// byte[] tmp = ecPrivKey.getEncoded();
+// KeyFactory keyFab = KeyFactory.getInstance("EC", gooProv);
+// keyFab.generatePrivate(new PKCS8EncodedKeySpec(tmp));
+// ECPrivateKeySpec ecPrivSpec = new ECPrivateKeySpec(ecPrivKey.getS(),
+// ecPrivKey.getParams());
+// keyFab.generatePrivate(ecPrivSpec);
+//
+// ECPublicKey ecPubKey = (ECPublicKey) ecKeyPair.getPublic();
+// tmp = ecPubKey.getEncoded(); // dont modify tmp now - is used below
+// keyFab.generatePublic(new X509EncodedKeySpec(tmp));
+// ECPublicKeySpec ecPubSpec = new ECPublicKeySpec(ecPubKey.getW(),
+// ecPubKey.getParams());
+// keyFab.generatePublic(ecPubSpec);
+//
+// Signature ecdsa = Signature.getInstance(ecdsas[s], gooProv);
+// ecdsa.initSign(ecPrivKey);
+// ecdsa.update(tmp);
+// byte[] sig = ecdsa.sign();
+// ecdsa.initVerify(ecPubKey);
+// ecdsa.update(tmp);
+// if (!ecdsa.verify(sig))
+// throw new RuntimeException("Signature not verified: "
+// + keyLengths[i]);
+//
+// // Cross verify using NSS if present
+// if (nssProv != null) {
+// keyFab = KeyFactory.getInstance("EC", nssProv);
+//
+// // For some reason NSS doesnt seem to work for P-192 and P-224?!
+// if (keyLengths[i] == 192 || keyLengths[i] == 224) continue;
+//
+// ECPrivateKey nssPrivKey = (ECPrivateKey) keyFab
+// .generatePrivate(new PKCS8EncodedKeySpec(ecPrivKey.getEncoded()));
+// ECPublicKey nssPubKey = (ECPublicKey) keyFab
+// .generatePublic(new X509EncodedKeySpec(ecPubKey.getEncoded()));
+//
+// ecdsa = Signature.getInstance(ecdsas[s], nssProv);
+// ecdsa.initVerify(nssPubKey);
+// ecdsa.update(tmp);
+// if (!ecdsa.verify(sig))
+// throw new RuntimeException("Signature not verified 2: "
+// + keyLengths[i]);
+//
+// ecdsa.initSign(nssPrivKey);
+// ecdsa.update(tmp);
+// sig = ecdsa.sign();
+// ecdsa = Signature.getInstance(ecdsas[s], gooProv);
+// ecdsa.initVerify(ecPubKey);
+// ecdsa.update(tmp);
+// if (!ecdsa.verify(sig))
+// throw new RuntimeException("Signature not verified 3: "
+// + keyLengths[i]);
+// }
+// }
+// System.out.println(" done");
+// }
+// }
+//
+// // Test Keyczar integration
+// // Signer ecdsaSigner = new Signer("c:\\temp\\eckeyset");
+// // String tbs = "Sign this";
+// // String sig = ecdsaSigner.sign(tbs);
+// // if (ecdsaSigner.verify(sig, tbs))
+// // System.out.println("Keyczar EC OK");
+// // else
+// // System.out.println("Keyczar EC not OK");
+// }
+//END connectbot-removed
+}
diff --git a/app/src/main/java/org/openintents/intents/FileManagerIntents.java b/app/src/main/java/org/openintents/intents/FileManagerIntents.java
new file mode 100644
index 0000000..fba2555
--- /dev/null
+++ b/app/src/main/java/org/openintents/intents/FileManagerIntents.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2008 OpenIntents.org
+ *
+ * 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.openintents.intents;
+
+// Version Dec 9, 2008
+
+
+/**
+ * Provides OpenIntents actions, extras, and categories used by providers.
+ * <p>These specifiers extend the standard Android specifiers.</p>
+ */
+public final class FileManagerIntents {
+
+ /**
+ * Activity Action: Pick a file through the file manager, or let user
+ * specify a custom file name.
+ * Data is the current file name or file name suggestion.
+ * Returns a new file name as file URI in data.
+ *
+ * <p>Constant Value: "org.openintents.action.PICK_FILE"</p>
+ */
+ public static final String ACTION_PICK_FILE = "org.openintents.action.PICK_FILE";
+
+ /**
+ * Activity Action: Pick a directory through the file manager, or let user
+ * specify a custom file name.
+ * Data is the current directory name or directory name suggestion.
+ * Returns a new directory name as file URI in data.
+ *
+ * <p>Constant Value: "org.openintents.action.PICK_DIRECTORY"</p>
+ */
+ public static final String ACTION_PICK_DIRECTORY = "org.openintents.action.PICK_DIRECTORY";
+
+ /**
+ * The title to display.
+ *
+ * <p>This is shown in the title bar of the file manager.</p>
+ *
+ * <p>Constant Value: "org.openintents.extra.TITLE"</p>
+ */
+ public static final String EXTRA_TITLE = "org.openintents.extra.TITLE";
+
+ /**
+ * The text on the button to display.
+ *
+ * <p>Depending on the use, it makes sense to set this to "Open" or "Save".</p>
+ *
+ * <p>Constant Value: "org.openintents.extra.BUTTON_TEXT"</p>
+ */
+ public static final String EXTRA_BUTTON_TEXT = "org.openintents.extra.BUTTON_TEXT";
+
+}