From 49b779dcaf03e3598d2709b321e20ea029b25163 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 1 Oct 2014 23:04:51 +0100 Subject: Convert to gradle build system --- .../harmony/niochar/charset/additional/IBM437.java | 423 +++++++ .../main/java/org/connectbot/ActionBarWrapper.java | 83 ++ .../main/java/org/connectbot/ColorsActivity.java | 331 ++++++ .../main/java/org/connectbot/ConsoleActivity.java | 1175 ++++++++++++++++++++ .../org/connectbot/GeneratePubkeyActivity.java | 353 ++++++ app/src/main/java/org/connectbot/HelpActivity.java | 77 ++ .../java/org/connectbot/HelpTopicActivity.java | 49 + .../java/org/connectbot/HostEditorActivity.java | 433 ++++++++ .../main/java/org/connectbot/HostListActivity.java | 570 ++++++++++ .../org/connectbot/PortForwardListActivity.java | 425 +++++++ .../java/org/connectbot/PubkeyListActivity.java | 673 +++++++++++ .../main/java/org/connectbot/SettingsActivity.java | 60 + .../main/java/org/connectbot/StrictModeSetup.java | 23 + app/src/main/java/org/connectbot/TerminalView.java | 452 ++++++++ .../main/java/org/connectbot/WizardActivity.java | 105 ++ .../java/org/connectbot/bean/AbstractBean.java | 49 + .../main/java/org/connectbot/bean/HostBean.java | 317 ++++++ .../java/org/connectbot/bean/PortForwardBean.java | 239 ++++ .../main/java/org/connectbot/bean/PubkeyBean.java | 234 ++++ .../java/org/connectbot/bean/SelectionArea.java | 201 ++++ .../java/org/connectbot/service/BackupAgent.java | 73 ++ .../java/org/connectbot/service/BackupWrapper.java | 71 ++ .../service/BridgeDisconnectedListener.java | 22 + .../org/connectbot/service/ConnectionNotifier.java | 192 ++++ .../connectbot/service/ConnectivityReceiver.java | 154 +++ .../service/FontSizeChangedListener.java | 31 + .../java/org/connectbot/service/KeyEventUtil.java | 98 ++ .../java/org/connectbot/service/PromptHelper.java | 159 +++ .../main/java/org/connectbot/service/Relay.java | 145 +++ .../org/connectbot/service/TerminalBridge.java | 1018 +++++++++++++++++ .../connectbot/service/TerminalKeyListener.java | 558 ++++++++++ .../org/connectbot/service/TerminalManager.java | 714 ++++++++++++ .../org/connectbot/transport/AbsTransport.java | 254 +++++ .../main/java/org/connectbot/transport/Local.java | 219 ++++ .../main/java/org/connectbot/transport/SSH.java | 958 ++++++++++++++++ .../main/java/org/connectbot/transport/Telnet.java | 330 ++++++ .../org/connectbot/transport/TransportFactory.java | 132 +++ app/src/main/java/org/connectbot/util/Colors.java | 91 ++ .../java/org/connectbot/util/EastAsianWidth.java | 75 ++ .../main/java/org/connectbot/util/Encryptor.java | 205 ++++ .../java/org/connectbot/util/EntropyDialog.java | 50 + .../main/java/org/connectbot/util/EntropyView.java | 169 +++ .../java/org/connectbot/util/HelpTopicView.java | 62 ++ .../java/org/connectbot/util/HostDatabase.java | 766 +++++++++++++ .../org/connectbot/util/OnDbWrittenListener.java | 26 + .../connectbot/util/OnEntropyGatheredListener.java | 22 + .../org/connectbot/util/PreferenceConstants.java | 90 ++ .../java/org/connectbot/util/PubkeyDatabase.java | 329 ++++++ .../main/java/org/connectbot/util/PubkeyUtils.java | 352 ++++++ .../connectbot/util/RobustSQLiteOpenHelper.java | 133 +++ .../org/connectbot/util/UberColorPickerDialog.java | 982 ++++++++++++++++ .../java/org/connectbot/util/VolumePreference.java | 72 ++ .../main/java/org/connectbot/util/XmlBuilder.java | 71 ++ app/src/main/java/org/keyczar/jce/EcCore.java | 679 +++++++++++ .../openintents/intents/FileManagerIntents.java | 66 ++ 55 files changed, 15640 insertions(+) create mode 100644 app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java create mode 100644 app/src/main/java/org/connectbot/ActionBarWrapper.java create mode 100644 app/src/main/java/org/connectbot/ColorsActivity.java create mode 100644 app/src/main/java/org/connectbot/ConsoleActivity.java create mode 100644 app/src/main/java/org/connectbot/GeneratePubkeyActivity.java create mode 100644 app/src/main/java/org/connectbot/HelpActivity.java create mode 100644 app/src/main/java/org/connectbot/HelpTopicActivity.java create mode 100644 app/src/main/java/org/connectbot/HostEditorActivity.java create mode 100644 app/src/main/java/org/connectbot/HostListActivity.java create mode 100644 app/src/main/java/org/connectbot/PortForwardListActivity.java create mode 100644 app/src/main/java/org/connectbot/PubkeyListActivity.java create mode 100644 app/src/main/java/org/connectbot/SettingsActivity.java create mode 100644 app/src/main/java/org/connectbot/StrictModeSetup.java create mode 100644 app/src/main/java/org/connectbot/TerminalView.java create mode 100644 app/src/main/java/org/connectbot/WizardActivity.java create mode 100644 app/src/main/java/org/connectbot/bean/AbstractBean.java create mode 100644 app/src/main/java/org/connectbot/bean/HostBean.java create mode 100644 app/src/main/java/org/connectbot/bean/PortForwardBean.java create mode 100644 app/src/main/java/org/connectbot/bean/PubkeyBean.java create mode 100644 app/src/main/java/org/connectbot/bean/SelectionArea.java create mode 100644 app/src/main/java/org/connectbot/service/BackupAgent.java create mode 100644 app/src/main/java/org/connectbot/service/BackupWrapper.java create mode 100644 app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java create mode 100644 app/src/main/java/org/connectbot/service/ConnectionNotifier.java create mode 100644 app/src/main/java/org/connectbot/service/ConnectivityReceiver.java create mode 100644 app/src/main/java/org/connectbot/service/FontSizeChangedListener.java create mode 100644 app/src/main/java/org/connectbot/service/KeyEventUtil.java create mode 100644 app/src/main/java/org/connectbot/service/PromptHelper.java create mode 100644 app/src/main/java/org/connectbot/service/Relay.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalBridge.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalKeyListener.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalManager.java create mode 100644 app/src/main/java/org/connectbot/transport/AbsTransport.java create mode 100644 app/src/main/java/org/connectbot/transport/Local.java create mode 100644 app/src/main/java/org/connectbot/transport/SSH.java create mode 100644 app/src/main/java/org/connectbot/transport/Telnet.java create mode 100644 app/src/main/java/org/connectbot/transport/TransportFactory.java create mode 100644 app/src/main/java/org/connectbot/util/Colors.java create mode 100644 app/src/main/java/org/connectbot/util/EastAsianWidth.java create mode 100644 app/src/main/java/org/connectbot/util/Encryptor.java create mode 100644 app/src/main/java/org/connectbot/util/EntropyDialog.java create mode 100644 app/src/main/java/org/connectbot/util/EntropyView.java create mode 100644 app/src/main/java/org/connectbot/util/HelpTopicView.java create mode 100644 app/src/main/java/org/connectbot/util/HostDatabase.java create mode 100644 app/src/main/java/org/connectbot/util/OnDbWrittenListener.java create mode 100644 app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java create mode 100644 app/src/main/java/org/connectbot/util/PreferenceConstants.java create mode 100644 app/src/main/java/org/connectbot/util/PubkeyDatabase.java create mode 100644 app/src/main/java/org/connectbot/util/PubkeyUtils.java create mode 100644 app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java create mode 100644 app/src/main/java/org/connectbot/util/UberColorPickerDialog.java create mode 100644 app/src/main/java/org/connectbot/util/VolumePreference.java create mode 100644 app/src/main/java/org/connectbot/util/XmlBuilder.java create mode 100644 app/src/main/java/org/keyczar/jce/EcCore.java create mode 100644 app/src/main/java/org/openintents/intents/FileManagerIntents.java (limited to 'app/src/main/java/org') 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= 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 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 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(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 contextRef; + + URLItemListener(Context context) { + this.contextRef = new WeakReference(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 values = new HashMap(); +// protected Map pubkeys = new HashMap(); + + 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 value) { + throw new UnsupportedOperationException("HostEditor Prefs do not support Set"); + } + } + + + public Editor edit() { + //Log.d(this.getClass().toString(), "edit()"); + return new Editor(); + } + + public Map 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 getStringSet(String key, Set defValue) { + throw new ClassCastException("HostEditor Prefs do not support Set"); + } + + protected List listeners = new LinkedList(); + + 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 pubkeyNicks = new LinkedList(Arrays.asList(pubkeyPref.getEntries())); + pubkeyNicks.addAll(pubkeydb.allValues(PubkeyDatabase.FIELD_PUBKEY_NICKNAME)); + pubkeyPref.setEntries(pubkeyNicks.toArray(new CharSequence[pubkeyNicks.size()])); + + List pubkeyIds = new LinkedList(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 charsetIdsList = new LinkedList(); + List charsetNamesList = new LinkedList(); + + for (Entry 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 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 transportSelection = new ArrayAdapter(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 { + private List 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 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 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 { + class ViewHolder { + public TextView nickname; + public TextView caption; + } + + private List portForwards; + + public PortForwardAdapter(Context context, List 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 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 byte[]. + */ + 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 names = new LinkedList(); + { + 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 { + private List pubkeys; + + class ViewHolder { + public TextView nickname; + public TextView caption; + public ImageView icon; + } + + public PubkeyAdapter(Context context, List 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 { + @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 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://.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 entry : values.valueSet()) { + Object value = entry.getValue(); + if (value != null) + xml.append(entry.getKey(), value); + } + xml.append(String.format("", 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 fontSizeChangedListeners; + + private final List 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(); + + fontSizeChangedListeners = new LinkedList(); + + 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(); + + fontSizeChangedListeners = new LinkedList(); + + 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 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 scanForURLs() { + List urls = new LinkedList(); + + 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. + *

+ * 1st press: next key to have meta state
+ * 2nd press: meta state is locked on
+ * 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 bridges = new LinkedList(); + public Map> mHostBridgeMap = + new HashMap>(); + public Map> mNicknameBridgeMap = + new HashMap>(); + + public TerminalBridge defaultBridge = null; + + public List disconnected = new LinkedList(); + + public Handler disconnectHandler = null; + + public Map loadedKeypairs = new HashMap(); + + 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> mPendingReconnect + = new LinkedList>(); + + 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 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 wr = new WeakReference(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 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 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 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 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(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 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 + * buffer at the start of offset and a maximum of + * length 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 options) { + // do nothing + } + + public Map 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 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 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 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 portForwards = new LinkedList(); + + 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 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 getOptions() { + Map options = new HashMap(); + + options.put("compression", Boolean.toString(compression)); + + return options; + } + + @Override + public void setOptions(Map 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 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 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 retrieveIdentities() { + Map pubKeys = new HashMap(manager.loadedKeypairs.size()); + + for (Entry 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.
+ * 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 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 selection = new HashMap(); + + 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 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(); + } + + 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 _id 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 getHosts(boolean sortColors) { + String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME; + List 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 createHostBeans(Cursor c) { + List hosts = new LinkedList(); + + 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 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 selection) { + StringBuilder selectionBuilder = new StringBuilder(); + + Iterator> i = selection.entrySet().iterator(); + + List selectionValuesList = new LinkedList(); + int n = 0; + while (i.hasNext()) { + Entry 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 getPortForwardsForHost(HostBean host) { + List portForwards = new LinkedList(); + + 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 _id 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 allPubkeys() { + return getPubkeys(null, null); + } + + public List getAllStartPubkeys() { + return getPubkeys(FIELD_PUBKEY_STARTUP + " = 1 AND " + FIELD_PUBKEY_ENCRYPTED + " = 0", null); + } + + private List getPubkeys(String selection, String[] selectionArgs) { + SQLiteDatabase db = getReadableDatabase(); + + List pubkeys = new LinkedList(); + + 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 _id ascending. + */ + public List allValues(String column) { + List list = new LinkedList(); + + 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 mTableNames = new LinkedList(); + private static List mIndexNames = new LinkedList(); + + 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.

+ * + * 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.

+ * + * @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). + } + } + + /** + * 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. + *

+ * 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. + *

+ * 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", field, + binary ? new String(Base64.encode(input.getBytes())) : input, field)); + } else if (data instanceof Integer) { + sb.append(String.format("<%s>%d", field, (Integer) data, field)); + } else if (data instanceof Long) { + sb.append(String.format("<%s>%d", field, (Long) data, field)); + } else if (data instanceof byte[]) { + sb.append(String.format("<%s>%s", field, new String(Base64.encode((byte[]) data)), field)); + } else if (data instanceof Boolean) { + sb.append(String.format("<%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() { +// @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 oidMap = new HashMap(); +// private static Map paramsMap = new HashMap(); +// private static Map friendlyNameMap = new HashMap(); +// +// 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); +// } +// +// // // +// // 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"); +// // // +// +// 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); +// } +// +// // // +// // 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"); +// // // +// +// 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. + *

These specifiers extend the standard Android specifiers.

+ */ +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. + * + *

Constant Value: "org.openintents.action.PICK_FILE"

+ */ + 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. + * + *

Constant Value: "org.openintents.action.PICK_DIRECTORY"

+ */ + public static final String ACTION_PICK_DIRECTORY = "org.openintents.action.PICK_DIRECTORY"; + + /** + * The title to display. + * + *

This is shown in the title bar of the file manager.

+ * + *

Constant Value: "org.openintents.extra.TITLE"

+ */ + public static final String EXTRA_TITLE = "org.openintents.extra.TITLE"; + + /** + * The text on the button to display. + * + *

Depending on the use, it makes sense to set this to "Open" or "Save".

+ * + *

Constant Value: "org.openintents.extra.BUTTON_TEXT"

+ */ + public static final String EXTRA_BUTTON_TEXT = "org.openintents.extra.BUTTON_TEXT"; + +} -- cgit v1.2.3