aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/org/connectbot/util
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/connectbot/util')
-rw-r--r--app/src/main/java/org/connectbot/util/Colors.java91
-rw-r--r--app/src/main/java/org/connectbot/util/EastAsianWidth.java75
-rw-r--r--app/src/main/java/org/connectbot/util/Encryptor.java205
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyDialog.java50
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyView.java169
-rw-r--r--app/src/main/java/org/connectbot/util/HelpTopicView.java62
-rw-r--r--app/src/main/java/org/connectbot/util/HostDatabase.java766
-rw-r--r--app/src/main/java/org/connectbot/util/OnDbWrittenListener.java26
-rw-r--r--app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java22
-rw-r--r--app/src/main/java/org/connectbot/util/PreferenceConstants.java90
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyDatabase.java329
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyUtils.java352
-rw-r--r--app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java133
-rw-r--r--app/src/main/java/org/connectbot/util/UberColorPickerDialog.java982
-rw-r--r--app/src/main/java/org/connectbot/util/VolumePreference.java72
-rw-r--r--app/src/main/java/org/connectbot/util/XmlBuilder.java71
16 files changed, 3495 insertions, 0 deletions
diff --git a/app/src/main/java/org/connectbot/util/Colors.java b/app/src/main/java/org/connectbot/util/Colors.java
new file mode 100644
index 0000000..ff88d68
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/Colors.java
@@ -0,0 +1,91 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Colors {
+ public final static Integer[] defaults = new Integer[] {
+ 0xff000000, // black
+ 0xffcc0000, // red
+ 0xff00cc00, // green
+ 0xffcccc00, // brown
+ 0xff0000cc, // blue
+ 0xffcc00cc, // purple
+ 0xff00cccc, // cyan
+ 0xffcccccc, // light grey
+ 0xff444444, // dark grey
+ 0xffff4444, // light red
+ 0xff44ff44, // light green
+ 0xffffff44, // yellow
+ 0xff4444ff, // light blue
+ 0xffff44ff, // light purple
+ 0xff44ffff, // light cyan
+ 0xffffffff, // white
+ 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7,
+ 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf,
+ 0xff005fd7, 0xff005fff, 0xff008700, 0xff00875f, 0xff008787,
+ 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f,
+ 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff, 0xff00d700,
+ 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff,
+ 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7,
+ 0xff00ffff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af,
+ 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87,
+ 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f8700, 0xff5f875f,
+ 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00,
+ 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
+ 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7,
+ 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf,
+ 0xff5fffd7, 0xff5fffff, 0xff870000, 0xff87005f, 0xff870087,
+ 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f,
+ 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff, 0xff878700,
+ 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff,
+ 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7,
+ 0xff87afff, 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af,
+ 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87,
+ 0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xffaf0000, 0xffaf005f,
+ 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00,
+ 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
+ 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7,
+ 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf,
+ 0xffafafd7, 0xffafafff, 0xffafd700, 0xffafd75f, 0xffafd787,
+ 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f,
+ 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffd70000,
+ 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff,
+ 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7,
+ 0xffd75fff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af,
+ 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87,
+ 0xffd7afaf, 0xffd7afd7, 0xffd7afff, 0xffd7d700, 0xffd7d75f,
+ 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00,
+ 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
+ 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7,
+ 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf,
+ 0xffff5fd7, 0xffff5fff, 0xffff8700, 0xffff875f, 0xffff8787,
+ 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f,
+ 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff, 0xffffd700,
+ 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff,
+ 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7,
+ 0xffffffff, 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626,
+ 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858,
+ 0xff626262, 0xff6c6c6c, 0xff767676, 0xff808080, 0xff8a8a8a,
+ 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc,
+ 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
+ };
+}
diff --git a/app/src/main/java/org/connectbot/util/EastAsianWidth.java b/app/src/main/java/org/connectbot/util/EastAsianWidth.java
new file mode 100644
index 0000000..0e274b5
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EastAsianWidth.java
@@ -0,0 +1,75 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import android.graphics.Paint;
+import android.text.AndroidCharacter;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class EastAsianWidth {
+ public static EastAsianWidth getInstance() {
+ if (PreferenceConstants.PRE_FROYO)
+ return PreFroyo.Holder.sInstance;
+ else
+ return FroyoAndBeyond.Holder.sInstance;
+ }
+
+ /**
+ * @param charArray
+ * @param i
+ * @param position
+ * @param wideAttribute
+ */
+ public abstract void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth);
+
+ private static class PreFroyo extends EastAsianWidth {
+ private static final int BUFFER_SIZE = 4096;
+ private float[] mWidths = new float[BUFFER_SIZE];
+
+ private static class Holder {
+ private static final PreFroyo sInstance = new PreFroyo();
+ }
+
+ @Override
+ public void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth) {
+ paint.getTextWidths(charArray, start, end, mWidths);
+ final int N = end - start;
+ for (int i = 0; i < N; i++)
+ wideAttribute[i] = (byte) (((int)mWidths[i] != charWidth) ?
+ AndroidCharacter.EAST_ASIAN_WIDTH_WIDE :
+ AndroidCharacter.EAST_ASIAN_WIDTH_NARROW);
+ }
+ }
+
+ private static class FroyoAndBeyond extends EastAsianWidth {
+ private static class Holder {
+ private static final FroyoAndBeyond sInstance = new FroyoAndBeyond();
+ }
+
+ @Override
+ public void measure(char[] charArray, int start, int end,
+ byte[] wideAttribute, Paint paint, int charWidth) {
+ AndroidCharacter.getEastAsianWidths(charArray, start, end - start, wideAttribute);
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/Encryptor.java b/app/src/main/java/org/connectbot/util/Encryptor.java
new file mode 100644
index 0000000..9d21454
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/Encryptor.java
@@ -0,0 +1,205 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+/**
+ * This class is from:
+ *
+ * Encryptor.java
+ * Copyright 2008 Zach Scrivena
+ * zachscrivena@gmail.com
+ * http://zs.freeshell.org/
+ */
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+/**
+ * Perform AES-128 encryption.
+ */
+public final class Encryptor
+{
+ /** name of the character set to use for converting between characters and bytes */
+ private static final String CHARSET_NAME = "UTF-8";
+
+ /** random number generator algorithm */
+ private static final String RNG_ALGORITHM = "SHA1PRNG";
+
+ /** message digest algorithm (must be sufficiently long to provide the key and initialization vector) */
+ private static final String DIGEST_ALGORITHM = "SHA-256";
+
+ /** key algorithm (must be compatible with CIPHER_ALGORITHM) */
+ private static final String KEY_ALGORITHM = "AES";
+
+ /** cipher algorithm (must be compatible with KEY_ALGORITHM) */
+ private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+
+
+ /**
+ * Private constructor that should never be called.
+ */
+ private Encryptor()
+ {}
+
+
+ /**
+ * Encrypt the specified cleartext using the given password.
+ * With the correct salt, number of iterations, and password, the decrypt() method reverses
+ * the effect of this method.
+ * This method generates and uses a random salt, and the user-specified number of iterations
+ * and password to create a 16-byte secret key and 16-byte initialization vector.
+ * The secret key and initialization vector are then used in the AES-128 cipher to encrypt
+ * the given cleartext.
+ *
+ * @param salt
+ * salt that was used in the encryption (to be populated)
+ * @param iterations
+ * number of iterations to use in salting
+ * @param password
+ * password to be used for encryption
+ * @param cleartext
+ * cleartext to be encrypted
+ * @return
+ * ciphertext
+ * @throws Exception
+ * on any error encountered in encryption
+ */
+ public static byte[] encrypt(
+ final byte[] salt,
+ final int iterations,
+ final String password,
+ final byte[] cleartext)
+ throws Exception
+ {
+ /* generate salt randomly */
+ SecureRandom.getInstance(RNG_ALGORITHM).nextBytes(salt);
+
+ /* compute key and initialization vector */
+ final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+ byte[] pw = password.getBytes(CHARSET_NAME);
+
+ for (int i = 0; i < iterations; i++)
+ {
+ /* add salt */
+ final byte[] salted = new byte[pw.length + salt.length];
+ System.arraycopy(pw, 0, salted, 0, pw.length);
+ System.arraycopy(salt, 0, salted, pw.length, salt.length);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* compute SHA-256 digest */
+ shaDigest.reset();
+ pw = shaDigest.digest(salted);
+ Arrays.fill(salted, (byte) 0x00);
+ }
+
+ /* extract the 16-byte key and initialization vector from the SHA-256 digest */
+ final byte[] key = new byte[16];
+ final byte[] iv = new byte[16];
+ System.arraycopy(pw, 0, key, 0, 16);
+ System.arraycopy(pw, 16, iv, 0, 16);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* perform AES-128 encryption */
+ final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ new SecretKeySpec(key, KEY_ALGORITHM),
+ new IvParameterSpec(iv));
+
+ Arrays.fill(key, (byte) 0x00);
+ Arrays.fill(iv, (byte) 0x00);
+
+ return cipher.doFinal(cleartext);
+ }
+
+
+ /**
+ * Decrypt the specified ciphertext using the given password.
+ * With the correct salt, number of iterations, and password, this method reverses the effect
+ * of the encrypt() method.
+ * This method uses the user-specified salt, number of iterations, and password
+ * to recreate the 16-byte secret key and 16-byte initialization vector.
+ * The secret key and initialization vector are then used in the AES-128 cipher to decrypt
+ * the given ciphertext.
+ *
+ * @param salt
+ * salt to be used in decryption
+ * @param iterations
+ * number of iterations to use in salting
+ * @param password
+ * password to be used for decryption
+ * @param ciphertext
+ * ciphertext to be decrypted
+ * @return
+ * cleartext
+ * @throws Exception
+ * on any error encountered in decryption
+ */
+ public static byte[] decrypt(
+ final byte[] salt,
+ final int iterations,
+ final String password,
+ final byte[] ciphertext)
+ throws Exception
+ {
+ /* compute key and initialization vector */
+ final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+ byte[] pw = password.getBytes(CHARSET_NAME);
+
+ for (int i = 0; i < iterations; i++)
+ {
+ /* add salt */
+ final byte[] salted = new byte[pw.length + salt.length];
+ System.arraycopy(pw, 0, salted, 0, pw.length);
+ System.arraycopy(salt, 0, salted, pw.length, salt.length);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* compute SHA-256 digest */
+ shaDigest.reset();
+ pw = shaDigest.digest(salted);
+ Arrays.fill(salted, (byte) 0x00);
+ }
+
+ /* extract the 16-byte key and initialization vector from the SHA-256 digest */
+ final byte[] key = new byte[16];
+ final byte[] iv = new byte[16];
+ System.arraycopy(pw, 0, key, 0, 16);
+ System.arraycopy(pw, 16, iv, 0, 16);
+ Arrays.fill(pw, (byte) 0x00);
+
+ /* perform AES-128 decryption */
+ final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ new SecretKeySpec(key, KEY_ALGORITHM),
+ new IvParameterSpec(iv));
+
+ Arrays.fill(key, (byte) 0x00);
+ Arrays.fill(iv, (byte) 0x00);
+
+ return cipher.doFinal(ciphertext);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/EntropyDialog.java b/app/src/main/java/org/connectbot/util/EntropyDialog.java
new file mode 100644
index 0000000..4498ce2
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EntropyDialog.java
@@ -0,0 +1,50 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import org.connectbot.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.View;
+
+public class EntropyDialog extends Dialog implements OnEntropyGatheredListener {
+
+ public EntropyDialog(Context context) {
+ super(context);
+
+ this.setContentView(R.layout.dia_gatherentropy);
+ this.setTitle(R.string.pubkey_gather_entropy);
+
+ ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this);
+ }
+
+ public EntropyDialog(Context context, View view) {
+ super(context);
+
+ this.setContentView(view);
+ this.setTitle(R.string.pubkey_gather_entropy);
+
+ ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this);
+ }
+
+ public void onEntropyGathered(byte[] entropy) {
+ this.dismiss();
+ }
+
+}
diff --git a/app/src/main/java/org/connectbot/util/EntropyView.java b/app/src/main/java/org/connectbot/util/EntropyView.java
new file mode 100644
index 0000000..c988673
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/EntropyView.java
@@ -0,0 +1,169 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import java.util.Vector;
+
+import org.connectbot.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.FontMetrics;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class EntropyView extends View {
+ private static final int SHA1_MAX_BYTES = 20;
+ private static final int MILLIS_BETWEEN_INPUTS = 50;
+
+ private Paint mPaint;
+ private FontMetrics mFontMetrics;
+ private boolean mFlipFlop;
+ private long mLastTime;
+ private Vector<OnEntropyGatheredListener> listeners;
+
+ private byte[] mEntropy;
+ private int mEntropyByteIndex;
+ private int mEntropyBitIndex;
+
+ private int splitText = 0;
+
+ private float lastX = 0.0f, lastY = 0.0f;
+
+ public EntropyView(Context context) {
+ super(context);
+
+ setUpEntropy();
+ }
+
+ public EntropyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setUpEntropy();
+ }
+
+ private void setUpEntropy() {
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setTypeface(Typeface.DEFAULT);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ mPaint.setTextSize(16);
+ mPaint.setColor(Color.WHITE);
+ mFontMetrics = mPaint.getFontMetrics();
+
+ mEntropy = new byte[SHA1_MAX_BYTES];
+ mEntropyByteIndex = 0;
+ mEntropyBitIndex = 0;
+
+ listeners = new Vector<OnEntropyGatheredListener>();
+ }
+
+ public void addOnEntropyGatheredListener(OnEntropyGatheredListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeOnEntropyGatheredListener(OnEntropyGatheredListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ String prompt = String.format(getResources().getString(R.string.pubkey_touch_prompt),
+ (int)(100.0 * (mEntropyByteIndex / 20.0)) + (int)(5.0 * (mEntropyBitIndex / 8.0)));
+ if (splitText > 0 ||
+ mPaint.measureText(prompt) > (getWidth() * 0.8)) {
+ if (splitText == 0)
+ splitText = prompt.indexOf(" ", prompt.length() / 2);
+
+ c.drawText(prompt.substring(0, splitText),
+ getWidth() / 2.0f,
+ getHeight() / 2.0f + (mPaint.ascent() + mPaint.descent()),
+ mPaint);
+ c.drawText(prompt.substring(splitText),
+ getWidth() / 2.0f,
+ getHeight() / 2.0f - (mPaint.ascent() + mPaint.descent()),
+ mPaint);
+ } else {
+ c.drawText(prompt,
+ getWidth() / 2.0f,
+ getHeight() / 2.0f - (mFontMetrics.ascent + mFontMetrics.descent) / 2,
+ mPaint);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mEntropyByteIndex >= SHA1_MAX_BYTES
+ || lastX == event.getX()
+ || lastY == event.getY())
+ return true;
+
+ // Only get entropy every 200 milliseconds to ensure the user has moved around.
+ long now = System.currentTimeMillis();
+ if ((now - mLastTime) < MILLIS_BETWEEN_INPUTS)
+ return true;
+ else
+ mLastTime = now;
+
+ byte input;
+
+ lastX = event.getX();
+ lastY = event.getY();
+
+ // Get the lowest 4 bits of each X, Y input and concat to the entropy-gathering
+ // string.
+ if (mFlipFlop)
+ input = (byte)((((int)lastX & 0x0F) << 4) | ((int)lastY & 0x0F));
+ else
+ input = (byte)((((int)lastY & 0x0F) << 4) | ((int)lastX & 0x0F));
+ mFlipFlop = !mFlipFlop;
+
+ for (int i = 0; i < 4 && mEntropyByteIndex < SHA1_MAX_BYTES; i++) {
+ if ((input & 0x3) == 0x1) {
+ mEntropy[mEntropyByteIndex] <<= 1;
+ mEntropy[mEntropyByteIndex] |= 1;
+ mEntropyBitIndex++;
+ input >>= 2;
+ } else if ((input & 0x3) == 0x2) {
+ mEntropy[mEntropyByteIndex] <<= 1;
+ mEntropyBitIndex++;
+ input >>= 2;
+ }
+
+ if (mEntropyBitIndex >= 8) {
+ mEntropyBitIndex = 0;
+ mEntropyByteIndex++;
+ }
+ }
+
+ // SHA1PRNG only keeps 160 bits of entropy.
+ if (mEntropyByteIndex >= SHA1_MAX_BYTES) {
+ for (OnEntropyGatheredListener listener: listeners) {
+ listener.onEntropyGathered(mEntropy);
+ }
+ }
+
+ invalidate();
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/HelpTopicView.java b/app/src/main/java/org/connectbot/util/HelpTopicView.java
new file mode 100644
index 0000000..0cbc267
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/HelpTopicView.java
@@ -0,0 +1,62 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import org.connectbot.HelpActivity;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpTopicView extends WebView {
+ public HelpTopicView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize();
+ }
+
+ public HelpTopicView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public HelpTopicView(Context context) {
+ super(context);
+ initialize();
+ }
+
+ private void initialize() {
+ WebSettings wSet = getSettings();
+ wSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+ wSet.setUseWideViewPort(false);
+ }
+
+ public HelpTopicView setTopic(String topic) {
+ String path = String.format("file:///android_asset/%s/%s%s",
+ HelpActivity.HELPDIR, topic, HelpActivity.SUFFIX);
+ loadUrl(path);
+
+ computeScroll();
+
+ return this;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/HostDatabase.java b/app/src/main/java/org/connectbot/util/HostDatabase.java
new file mode 100644
index 0000000..2a92bab
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/HostDatabase.java
@@ -0,0 +1,766 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.trilead.ssh2.KnownHosts;
+
+/**
+ * Contains information about various SSH hosts, include public hostkey if known
+ * from previous sessions.
+ *
+ * @author jsharkey
+ */
+public class HostDatabase extends RobustSQLiteOpenHelper {
+
+ public final static String TAG = "ConnectBot.HostDatabase";
+
+ public final static String DB_NAME = "hosts";
+ public final static int DB_VERSION = 22;
+
+ public final static String TABLE_HOSTS = "hosts";
+ public final static String FIELD_HOST_NICKNAME = "nickname";
+ public final static String FIELD_HOST_PROTOCOL = "protocol";
+ public final static String FIELD_HOST_USERNAME = "username";
+ public final static String FIELD_HOST_HOSTNAME = "hostname";
+ public final static String FIELD_HOST_PORT = "port";
+ public final static String FIELD_HOST_HOSTKEYALGO = "hostkeyalgo";
+ public final static String FIELD_HOST_HOSTKEY = "hostkey";
+ public final static String FIELD_HOST_LASTCONNECT = "lastconnect";
+ public final static String FIELD_HOST_COLOR = "color";
+ public final static String FIELD_HOST_USEKEYS = "usekeys";
+ public final static String FIELD_HOST_USEAUTHAGENT = "useauthagent";
+ public final static String FIELD_HOST_POSTLOGIN = "postlogin";
+ public final static String FIELD_HOST_PUBKEYID = "pubkeyid";
+ public final static String FIELD_HOST_WANTSESSION = "wantsession";
+ public final static String FIELD_HOST_DELKEY = "delkey";
+ public final static String FIELD_HOST_FONTSIZE = "fontsize";
+ public final static String FIELD_HOST_COMPRESSION = "compression";
+ public final static String FIELD_HOST_ENCODING = "encoding";
+ public final static String FIELD_HOST_STAYCONNECTED = "stayconnected";
+
+ public final static String TABLE_PORTFORWARDS = "portforwards";
+ public final static String FIELD_PORTFORWARD_HOSTID = "hostid";
+ public final static String FIELD_PORTFORWARD_NICKNAME = "nickname";
+ public final static String FIELD_PORTFORWARD_TYPE = "type";
+ public final static String FIELD_PORTFORWARD_SOURCEPORT = "sourceport";
+ public final static String FIELD_PORTFORWARD_DESTADDR = "destaddr";
+ public final static String FIELD_PORTFORWARD_DESTPORT = "destport";
+
+ public final static String TABLE_COLORS = "colors";
+ public final static String FIELD_COLOR_SCHEME = "scheme";
+ public final static String FIELD_COLOR_NUMBER = "number";
+ public final static String FIELD_COLOR_VALUE = "value";
+
+ public final static String TABLE_COLOR_DEFAULTS = "colorDefaults";
+ public final static String FIELD_COLOR_FG = "fg";
+ public final static String FIELD_COLOR_BG = "bg";
+
+ public final static int DEFAULT_FG_COLOR = 7;
+ public final static int DEFAULT_BG_COLOR = 0;
+
+ public final static String COLOR_RED = "red";
+ public final static String COLOR_GREEN = "green";
+ public final static String COLOR_BLUE = "blue";
+ public final static String COLOR_GRAY = "gray";
+
+ public final static String PORTFORWARD_LOCAL = "local";
+ public final static String PORTFORWARD_REMOTE = "remote";
+ public final static String PORTFORWARD_DYNAMIC4 = "dynamic4";
+ public final static String PORTFORWARD_DYNAMIC5 = "dynamic5";
+
+ public final static String DELKEY_DEL = "del";
+ public final static String DELKEY_BACKSPACE = "backspace";
+
+ public final static String AUTHAGENT_NO = "no";
+ public final static String AUTHAGENT_CONFIRM = "confirm";
+ public final static String AUTHAGENT_YES = "yes";
+
+ public final static String ENCODING_DEFAULT = Charset.defaultCharset().name();
+
+ public final static long PUBKEYID_NEVER = -2;
+ public final static long PUBKEYID_ANY = -1;
+
+ public static final int DEFAULT_COLOR_SCHEME = 0;
+
+ // Table creation strings
+ public static final String CREATE_TABLE_COLOR_DEFAULTS =
+ "CREATE TABLE " + TABLE_COLOR_DEFAULTS
+ + " (" + FIELD_COLOR_SCHEME + " INTEGER NOT NULL, "
+ + FIELD_COLOR_FG + " INTEGER NOT NULL DEFAULT " + DEFAULT_FG_COLOR + ", "
+ + FIELD_COLOR_BG + " INTEGER NOT NULL DEFAULT " + DEFAULT_BG_COLOR + ")";
+ public static final String CREATE_TABLE_COLOR_DEFAULTS_INDEX =
+ "CREATE INDEX " + TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLOR_DEFAULTS + " (" + FIELD_COLOR_SCHEME + ");";
+
+ private static final String WHERE_SCHEME_AND_COLOR = FIELD_COLOR_SCHEME + " = ? AND "
+ + FIELD_COLOR_NUMBER + " = ?";
+
+ static {
+ addTableName(TABLE_HOSTS);
+ addTableName(TABLE_PORTFORWARDS);
+ addIndexName(TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index");
+ addTableName(TABLE_COLORS);
+ addIndexName(TABLE_COLORS + FIELD_COLOR_SCHEME + "index");
+ addTableName(TABLE_COLOR_DEFAULTS);
+ addIndexName(TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index");
+ }
+
+ public static final Object[] dbLock = new Object[0];
+
+ public HostDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+
+ getWritableDatabase().close();
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ super.onCreate(db);
+
+ db.execSQL("CREATE TABLE " + TABLE_HOSTS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_HOST_NICKNAME + " TEXT, "
+ + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh', "
+ + FIELD_HOST_USERNAME + " TEXT, "
+ + FIELD_HOST_HOSTNAME + " TEXT, "
+ + FIELD_HOST_PORT + " INTEGER, "
+ + FIELD_HOST_HOSTKEYALGO + " TEXT, "
+ + FIELD_HOST_HOSTKEY + " BLOB, "
+ + FIELD_HOST_LASTCONNECT + " INTEGER, "
+ + FIELD_HOST_COLOR + " TEXT, "
+ + FIELD_HOST_USEKEYS + " TEXT, "
+ + FIELD_HOST_USEAUTHAGENT + " TEXT, "
+ + FIELD_HOST_POSTLOGIN + " TEXT, "
+ + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY + ", "
+ + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "', "
+ + FIELD_HOST_FONTSIZE + " INTEGER, "
+ + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "', "
+ + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', "
+ + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "', "
+ + FIELD_HOST_STAYCONNECTED + " TEXT)");
+
+ db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
+ + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
+ + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
+ + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
+ + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
+ + FIELD_PORTFORWARD_DESTPORT + " TEXT)");
+
+ db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON "
+ + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");");
+
+ db.execSQL("CREATE TABLE " + TABLE_COLORS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_COLOR_NUMBER + " INTEGER, "
+ + FIELD_COLOR_VALUE + " INTEGER, "
+ + FIELD_COLOR_SCHEME + " INTEGER)");
+
+ db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");");
+
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX);
+ }
+
+ @Override
+ public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException {
+ // Versions of the database before the Android Market release will be
+ // shot without warning.
+ if (oldVersion <= 9) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_HOSTS);
+ onCreate(db);
+ return;
+ }
+
+ switch (oldVersion) {
+ case 10:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY);
+ case 11:
+ db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
+ + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
+ + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
+ + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
+ + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
+ + FIELD_PORTFORWARD_DESTPORT + " INTEGER)");
+ case 12:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "'");
+ case 13:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "'");
+ case 14:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "'");
+ case 15:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh'");
+ case 16:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "'");
+ case 17:
+ db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON "
+ + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");");
+
+ // Add colors
+ db.execSQL("CREATE TABLE " + TABLE_COLORS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_COLOR_NUMBER + " INTEGER, "
+ + FIELD_COLOR_VALUE + " INTEGER, "
+ + FIELD_COLOR_SCHEME + " INTEGER)");
+ db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON "
+ + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");");
+ case 18:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_USEAUTHAGENT + " TEXT DEFAULT '" + AUTHAGENT_NO + "'");
+ case 19:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_STAYCONNECTED + " TEXT");
+ case 20:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_FONTSIZE + " INTEGER");
+ case 21:
+ db.execSQL("DROP TABLE " + TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS);
+ db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX);
+ }
+ }
+
+ /**
+ * Touch a specific host to update its "last connected" field.
+ * @param nickname Nickname field of host to update
+ */
+ public void touchHost(HostBean host) {
+ long now = System.currentTimeMillis() / 1000;
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_LASTCONNECT, now);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ db.update(TABLE_HOSTS, values, "_id = ?", new String[] { String.valueOf(host.getId()) });
+ }
+ }
+
+ /**
+ * Create a new host using the given parameters.
+ */
+ public HostBean saveHost(HostBean host) {
+ long id;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ id = db.insert(TABLE_HOSTS, null, host.getValues());
+ }
+
+ host.setId(id);
+
+ return host;
+ }
+
+ /**
+ * Update a field in a host record.
+ */
+ public boolean updateFontSize(HostBean host) {
+ long id = host.getId();
+ if (id < 0)
+ return false;
+
+ ContentValues updates = new ContentValues();
+ updates.put(FIELD_HOST_FONTSIZE, host.getFontSize());
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ db.update(TABLE_HOSTS, updates, "_id = ?",
+ new String[] { String.valueOf(id) });
+
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete a specific host by its <code>_id</code> value.
+ */
+ public void deleteHost(HostBean host) {
+ if (host.getId() < 0)
+ return;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.delete(TABLE_HOSTS, "_id = ?", new String[] { String.valueOf(host.getId()) });
+ }
+ }
+
+ /**
+ * Return a cursor that contains information about all known hosts.
+ * @param sortColors If true, sort by color, otherwise sort by nickname.
+ */
+ public List<HostBean> getHosts(boolean sortColors) {
+ String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME;
+ List<HostBean> hosts;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null, null, null, null, null, sortField + " ASC");
+
+ hosts = createHostBeans(c);
+
+ c.close();
+ }
+
+ return hosts;
+ }
+
+ /**
+ * @param hosts
+ * @param c
+ */
+ private List<HostBean> createHostBeans(Cursor c) {
+ List<HostBean> hosts = new LinkedList<HostBean>();
+
+ final int COL_ID = c.getColumnIndexOrThrow("_id"),
+ COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_HOST_NICKNAME),
+ COL_PROTOCOL = c.getColumnIndexOrThrow(FIELD_HOST_PROTOCOL),
+ COL_USERNAME = c.getColumnIndexOrThrow(FIELD_HOST_USERNAME),
+ COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME),
+ COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT),
+ COL_LASTCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT),
+ COL_COLOR = c.getColumnIndexOrThrow(FIELD_HOST_COLOR),
+ COL_USEKEYS = c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS),
+ COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT),
+ COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN),
+ COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID),
+ COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION),
+ COL_DELKEY = c.getColumnIndexOrThrow(FIELD_HOST_DELKEY),
+ COL_FONTSIZE = c.getColumnIndexOrThrow(FIELD_HOST_FONTSIZE),
+ COL_COMPRESSION = c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION),
+ COL_ENCODING = c.getColumnIndexOrThrow(FIELD_HOST_ENCODING),
+ COL_STAYCONNECTED = c.getColumnIndexOrThrow(FIELD_HOST_STAYCONNECTED);
+
+
+ while (c.moveToNext()) {
+ HostBean host = new HostBean();
+
+ host.setId(c.getLong(COL_ID));
+ host.setNickname(c.getString(COL_NICKNAME));
+ host.setProtocol(c.getString(COL_PROTOCOL));
+ host.setUsername(c.getString(COL_USERNAME));
+ host.setHostname(c.getString(COL_HOSTNAME));
+ host.setPort(c.getInt(COL_PORT));
+ host.setLastConnect(c.getLong(COL_LASTCONNECT));
+ host.setColor(c.getString(COL_COLOR));
+ host.setUseKeys(Boolean.valueOf(c.getString(COL_USEKEYS)));
+ host.setUseAuthAgent(c.getString(COL_USEAUTHAGENT));
+ host.setPostLogin(c.getString(COL_POSTLOGIN));
+ host.setPubkeyId(c.getLong(COL_PUBKEYID));
+ host.setWantSession(Boolean.valueOf(c.getString(COL_WANTSESSION)));
+ host.setDelKey(c.getString(COL_DELKEY));
+ host.setFontSize(c.getInt(COL_FONTSIZE));
+ host.setCompression(Boolean.valueOf(c.getString(COL_COMPRESSION)));
+ host.setEncoding(c.getString(COL_ENCODING));
+ host.setStayConnected(Boolean.valueOf(c.getString(COL_STAYCONNECTED)));
+
+ hosts.add(host);
+ }
+
+ return hosts;
+ }
+
+ /**
+ * @param c
+ * @return
+ */
+ private HostBean getFirstHostBean(Cursor c) {
+ HostBean host = null;
+
+ List<HostBean> hosts = createHostBeans(c);
+ if (hosts.size() > 0)
+ host = hosts.get(0);
+
+ c.close();
+
+ return host;
+ }
+
+ /**
+ * @param nickname
+ * @param protocol
+ * @param username
+ * @param hostname
+ * @param hostname2
+ * @param port
+ * @return
+ */
+ public HostBean findHost(Map<String, String> selection) {
+ StringBuilder selectionBuilder = new StringBuilder();
+
+ Iterator<Entry<String, String>> i = selection.entrySet().iterator();
+
+ List<String> selectionValuesList = new LinkedList<String>();
+ int n = 0;
+ while (i.hasNext()) {
+ Entry<String, String> entry = i.next();
+
+ if (entry.getValue() == null)
+ continue;
+
+ if (n++ > 0)
+ selectionBuilder.append(" AND ");
+
+ selectionBuilder.append(entry.getKey())
+ .append(" = ?");
+
+ selectionValuesList.add(entry.getValue());
+ }
+
+ String selectionValues[] = new String[selectionValuesList.size()];
+ selectionValuesList.toArray(selectionValues);
+ selectionValuesList = null;
+
+ HostBean host;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null,
+ selectionBuilder.toString(),
+ selectionValues,
+ null, null, null);
+
+ host = getFirstHostBean(c);
+ }
+
+ return host;
+ }
+
+ /**
+ * @param hostId
+ * @return
+ */
+ public HostBean findHostById(long hostId) {
+ HostBean host;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_HOSTS, null,
+ "_id = ?", new String[] { String.valueOf(hostId) },
+ null, null, null);
+
+ host = getFirstHostBean(c);
+ }
+
+ return host;
+ }
+
+ /**
+ * Record the given hostkey into database under this nickname.
+ * @param hostname
+ * @param port
+ * @param hostkeyalgo
+ * @param hostkey
+ */
+ public void saveKnownHost(String hostname, int port, String hostkeyalgo, byte[] hostkey) {
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_HOSTKEYALGO, hostkeyalgo);
+ values.put(FIELD_HOST_HOSTKEY, hostkey);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ db.update(TABLE_HOSTS, values,
+ FIELD_HOST_HOSTNAME + " = ? AND " + FIELD_HOST_PORT + " = ?",
+ new String[] { hostname, String.valueOf(port) });
+ Log.d(TAG, String.format("Finished saving hostkey information for '%s'", hostname));
+ }
+ }
+
+ /**
+ * Build list of known hosts for Trilead library.
+ * @return
+ */
+ public KnownHosts getKnownHosts() {
+ KnownHosts known = new KnownHosts();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_HOSTS, new String[] { FIELD_HOST_HOSTNAME,
+ FIELD_HOST_PORT, FIELD_HOST_HOSTKEYALGO, FIELD_HOST_HOSTKEY },
+ null, null, null, null, null);
+
+ if (c != null) {
+ int COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME),
+ COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT),
+ COL_HOSTKEYALGO = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEYALGO),
+ COL_HOSTKEY = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEY);
+
+ while (c.moveToNext()) {
+ String hostname = c.getString(COL_HOSTNAME),
+ hostkeyalgo = c.getString(COL_HOSTKEYALGO);
+ int port = c.getInt(COL_PORT);
+ byte[] hostkey = c.getBlob(COL_HOSTKEY);
+
+ if (hostkeyalgo == null || hostkeyalgo.length() == 0) continue;
+ if (hostkey == null || hostkey.length == 0) continue;
+
+ try {
+ known.addHostkey(new String[] { String.format("%s:%d", hostname, port) }, hostkeyalgo, hostkey);
+ } catch(Exception e) {
+ Log.e(TAG, "Problem while adding a known host from database", e);
+ }
+ }
+
+ c.close();
+ }
+ }
+
+ return known;
+ }
+
+ /**
+ * Unset any hosts using a pubkey ID that has been deleted.
+ * @param pubkeyId
+ */
+ public void stopUsingPubkey(long pubkeyId) {
+ if (pubkeyId < 0) return;
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_HOST_PUBKEYID, PUBKEYID_ANY);
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ db.update(TABLE_HOSTS, values, FIELD_HOST_PUBKEYID + " = ?", new String[] { String.valueOf(pubkeyId) });
+ }
+
+ Log.d(TAG, String.format("Set all hosts using pubkey id %d to -1", pubkeyId));
+ }
+
+ /*
+ * Methods for dealing with port forwards attached to hosts
+ */
+
+ /**
+ * Returns a list of all the port forwards associated with a particular host ID.
+ * @param host the host for which we want the port forward list
+ * @return port forwards associated with host ID
+ */
+ public List<PortForwardBean> getPortForwardsForHost(HostBean host) {
+ List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getReadableDatabase();
+
+ Cursor c = db.query(TABLE_PORTFORWARDS, new String[] {
+ "_id", FIELD_PORTFORWARD_NICKNAME, FIELD_PORTFORWARD_TYPE, FIELD_PORTFORWARD_SOURCEPORT,
+ FIELD_PORTFORWARD_DESTADDR, FIELD_PORTFORWARD_DESTPORT },
+ FIELD_PORTFORWARD_HOSTID + " = ?", new String[] { String.valueOf(host.getId()) },
+ null, null, null);
+
+ while (c.moveToNext()) {
+ PortForwardBean pfb = new PortForwardBean(
+ c.getInt(0),
+ host.getId(),
+ c.getString(1),
+ c.getString(2),
+ c.getInt(3),
+ c.getString(4),
+ c.getInt(5));
+ portForwards.add(pfb);
+ }
+
+ c.close();
+ }
+
+ return portForwards;
+ }
+
+ /**
+ * Update the parameters of a port forward in the database.
+ * @param pfb {@link PortForwardBean} to save
+ * @return true on success
+ */
+ public boolean savePortForward(PortForwardBean pfb) {
+ boolean success = false;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ if (pfb.getId() < 0) {
+ long id = db.insert(TABLE_PORTFORWARDS, null, pfb.getValues());
+ pfb.setId(id);
+ success = true;
+ } else {
+ if (db.update(TABLE_PORTFORWARDS, pfb.getValues(), "_id = ?", new String[] { String.valueOf(pfb.getId()) }) > 0)
+ success = true;
+ }
+ }
+
+ return success;
+ }
+
+ /**
+ * Deletes a port forward from the database.
+ * @param pfb {@link PortForwardBean} to delete
+ */
+ public void deletePortForward(PortForwardBean pfb) {
+ if (pfb.getId() < 0)
+ return;
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.delete(TABLE_PORTFORWARDS, "_id = ?", new String[] { String.valueOf(pfb.getId()) });
+ }
+ }
+
+ public Integer[] getColorsForScheme(int scheme) {
+ Integer[] colors = Colors.defaults.clone();
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_COLORS, new String[] {
+ FIELD_COLOR_NUMBER, FIELD_COLOR_VALUE },
+ FIELD_COLOR_SCHEME + " = ?",
+ new String[] { String.valueOf(scheme) },
+ null, null, null);
+
+ while (c.moveToNext()) {
+ colors[c.getInt(0)] = new Integer(c.getInt(1));
+ }
+
+ c.close();
+ }
+
+ return colors;
+ }
+
+ public void setColorForScheme(int scheme, int number, int value) {
+ final SQLiteDatabase db;
+
+ final String[] whereArgs = new String[] { String.valueOf(scheme), String.valueOf(number) };
+
+ if (value == Colors.defaults[number]) {
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ db.delete(TABLE_COLORS,
+ WHERE_SCHEME_AND_COLOR, whereArgs);
+ }
+ } else {
+ final ContentValues values = new ContentValues();
+ values.put(FIELD_COLOR_VALUE, value);
+
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ final int rowsAffected = db.update(TABLE_COLORS, values,
+ WHERE_SCHEME_AND_COLOR, whereArgs);
+
+ if (rowsAffected == 0) {
+ values.put(FIELD_COLOR_SCHEME, scheme);
+ values.put(FIELD_COLOR_NUMBER, number);
+ db.insert(TABLE_COLORS, null, values);
+ }
+ }
+ }
+ }
+
+ public void setGlobalColor(int number, int value) {
+ setColorForScheme(DEFAULT_COLOR_SCHEME, number, value);
+ }
+
+ public int[] getDefaultColorsForScheme(int scheme) {
+ int[] colors = new int[] { DEFAULT_FG_COLOR, DEFAULT_BG_COLOR };
+
+ synchronized (dbLock) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_COLOR_DEFAULTS,
+ new String[] { FIELD_COLOR_FG, FIELD_COLOR_BG },
+ FIELD_COLOR_SCHEME + " = ?",
+ new String[] { String.valueOf(scheme) },
+ null, null, null);
+
+ if (c.moveToFirst()) {
+ colors[0] = c.getInt(0);
+ colors[1] = c.getInt(1);
+ }
+
+ c.close();
+ }
+
+ return colors;
+ }
+
+ public int[] getGlobalDefaultColors() {
+ return getDefaultColorsForScheme(DEFAULT_COLOR_SCHEME);
+ }
+
+ public void setDefaultColorsForScheme(int scheme, int fg, int bg) {
+ SQLiteDatabase db;
+
+ String schemeWhere = null;
+ String[] whereArgs;
+
+ schemeWhere = FIELD_COLOR_SCHEME + " = ?";
+ whereArgs = new String[] { String.valueOf(scheme) };
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_COLOR_FG, fg);
+ values.put(FIELD_COLOR_BG, bg);
+
+ synchronized (dbLock) {
+ db = getWritableDatabase();
+
+ int rowsAffected = db.update(TABLE_COLOR_DEFAULTS, values,
+ schemeWhere, whereArgs);
+
+ if (rowsAffected == 0) {
+ values.put(FIELD_COLOR_SCHEME, scheme);
+ db.insert(TABLE_COLOR_DEFAULTS, null, values);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java
new file mode 100644
index 0000000..ef33797
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java
@@ -0,0 +1,26 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+/**
+ * @author kroot
+ *
+ */
+public interface OnDbWrittenListener {
+ public void onDbWritten();
+}
diff --git a/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java
new file mode 100644
index 0000000..5debd65
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java
@@ -0,0 +1,22 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+public interface OnEntropyGatheredListener {
+ void onEntropyGathered(byte[] entropy);
+}
diff --git a/app/src/main/java/org/connectbot/util/PreferenceConstants.java b/app/src/main/java/org/connectbot/util/PreferenceConstants.java
new file mode 100644
index 0000000..e9fb06c
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PreferenceConstants.java
@@ -0,0 +1,90 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import android.os.Build;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class PreferenceConstants {
+ public static final int SDK_INT = Integer.parseInt(Build.VERSION.SDK);
+ public static final boolean PRE_ECLAIR = SDK_INT < 5;
+ public static final boolean PRE_FROYO = SDK_INT < 8;
+ public static final boolean PRE_HONEYCOMB = SDK_INT < 11;
+
+ public static final String MEMKEYS = "memkeys";
+ public static final String UPDATE = "update";
+
+ public static final String UPDATE_DAILY = "Daily";
+ public static final String UPDATE_WEEKLY = "Weekly";
+ public static final String UPDATE_NEVER = "Never";
+
+ public static final String LAST_CHECKED = "lastchecked";
+
+ public static final String SCROLLBACK = "scrollback";
+
+ public static final String EMULATION = "emulation";
+
+ public static final String ROTATION = "rotation";
+
+ public static final String ROTATION_DEFAULT = "Default";
+ public static final String ROTATION_LANDSCAPE = "Force landscape";
+ public static final String ROTATION_PORTRAIT = "Force portrait";
+ public static final String ROTATION_AUTOMATIC = "Automatic";
+
+ public static final String FULLSCREEN = "fullscreen";
+
+ public static final String KEYMODE = "keymode";
+
+ public static final String KEYMODE_RIGHT = "Use right-side keys";
+ public static final String KEYMODE_LEFT = "Use left-side keys";
+
+ public static final String CAMERA = "camera";
+
+ public static final String CAMERA_CTRLA_SPACE = "Ctrl+A then Space";
+ public static final String CAMERA_CTRLA = "Ctrl+A";
+ public static final String CAMERA_ESC = "Esc";
+ public static final String CAMERA_ESC_A = "Esc+A";
+
+ public static final String KEEP_ALIVE = "keepalive";
+
+ public static final String WIFI_LOCK = "wifilock";
+
+ public static final String BUMPY_ARROWS = "bumpyarrows";
+
+ public static final String EULA = "eula";
+
+ public static final String SORT_BY_COLOR = "sortByColor";
+
+ public static final String BELL = "bell";
+ public static final String BELL_VOLUME = "bellVolume";
+ public static final String BELL_VIBRATE = "bellVibrate";
+ public static final String BELL_NOTIFICATION = "bellNotification";
+ public static final float DEFAULT_BELL_VOLUME = 0.25f;
+
+ public static final String CONNECTION_PERSIST = "connPersist";
+
+ public static final String SHIFT_FKEYS = "shiftfkeys";
+ public static final String CTRL_FKEYS = "ctrlfkeys";
+ public static final String VOLUME_FONT = "volumefont";
+
+ /* Backup identifiers */
+ public static final String BACKUP_PREF_KEY = "prefs";
+}
diff --git a/app/src/main/java/org/connectbot/util/PubkeyDatabase.java b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java
new file mode 100644
index 0000000..a8993cb
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java
@@ -0,0 +1,329 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.connectbot.bean.PubkeyBean;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+/**
+ * Public Key Encryption database. Contains private and public key pairs
+ * for public key authentication.
+ *
+ * @author Kenny Root
+ */
+public class PubkeyDatabase extends RobustSQLiteOpenHelper {
+ public final static String TAG = "ConnectBot.PubkeyDatabase";
+
+ public final static String DB_NAME = "pubkeys";
+ public final static int DB_VERSION = 2;
+
+ public final static String TABLE_PUBKEYS = "pubkeys";
+ public final static String FIELD_PUBKEY_NICKNAME = "nickname";
+ public final static String FIELD_PUBKEY_TYPE = "type";
+ public final static String FIELD_PUBKEY_PRIVATE = "private";
+ public final static String FIELD_PUBKEY_PUBLIC = "public";
+ public final static String FIELD_PUBKEY_ENCRYPTED = "encrypted";
+ public final static String FIELD_PUBKEY_STARTUP = "startup";
+ public final static String FIELD_PUBKEY_CONFIRMUSE = "confirmuse";
+ public final static String FIELD_PUBKEY_LIFETIME = "lifetime";
+
+ public final static String KEY_TYPE_RSA = "RSA",
+ KEY_TYPE_DSA = "DSA",
+ KEY_TYPE_IMPORTED = "IMPORTED",
+ KEY_TYPE_EC = "EC";
+
+ private Context context;
+
+ static {
+ addTableName(TABLE_PUBKEYS);
+ }
+
+ public PubkeyDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+
+ this.context = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ super.onCreate(db);
+
+ db.execSQL("CREATE TABLE " + TABLE_PUBKEYS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PUBKEY_NICKNAME + " TEXT, "
+ + FIELD_PUBKEY_TYPE + " TEXT, "
+ + FIELD_PUBKEY_PRIVATE + " BLOB, "
+ + FIELD_PUBKEY_PUBLIC + " BLOB, "
+ + FIELD_PUBKEY_ENCRYPTED + " INTEGER, "
+ + FIELD_PUBKEY_STARTUP + " INTEGER, "
+ + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0, "
+ + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0)");
+ }
+
+ @Override
+ public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException {
+ switch (oldVersion) {
+ case 1:
+ db.execSQL("ALTER TABLE " + TABLE_PUBKEYS
+ + " ADD COLUMN " + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE " + TABLE_PUBKEYS
+ + " ADD COLUMN " + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0");
+ }
+ }
+
+ /**
+ * Delete a specific host by its <code>_id</code> value.
+ */
+ public void deletePubkey(PubkeyBean pubkey) {
+ HostDatabase hostdb = new HostDatabase(context);
+ hostdb.stopUsingPubkey(pubkey.getId());
+ hostdb.close();
+
+ SQLiteDatabase db = getWritableDatabase();
+ db.delete(TABLE_PUBKEYS, "_id = ?", new String[] { Long.toString(pubkey.getId()) });
+ db.close();
+ }
+
+ /**
+ * Return a cursor that contains information about all known hosts.
+ */
+ /*
+ public Cursor allPubkeys() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ return db.query(TABLE_PUBKEYS, new String[] { "_id",
+ FIELD_PUBKEY_NICKNAME, FIELD_PUBKEY_TYPE, FIELD_PUBKEY_PRIVATE,
+ FIELD_PUBKEY_PUBLIC, FIELD_PUBKEY_ENCRYPTED, FIELD_PUBKEY_STARTUP },
+ null, null, null, null, null);
+ }*/
+
+ public List<PubkeyBean> allPubkeys() {
+ return getPubkeys(null, null);
+ }
+
+ public List<PubkeyBean> getAllStartPubkeys() {
+ return getPubkeys(FIELD_PUBKEY_STARTUP + " = 1 AND " + FIELD_PUBKEY_ENCRYPTED + " = 0", null);
+ }
+
+ private List<PubkeyBean> getPubkeys(String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ List<PubkeyBean> pubkeys = new LinkedList<PubkeyBean>();
+
+ Cursor c = db.query(TABLE_PUBKEYS, null, selection, selectionArgs, null, null, null);
+
+ if (c != null) {
+ final int COL_ID = c.getColumnIndexOrThrow("_id"),
+ COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME),
+ COL_TYPE = c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE),
+ COL_PRIVATE = c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE),
+ COL_PUBLIC = c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC),
+ COL_ENCRYPTED = c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED),
+ COL_STARTUP = c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP),
+ COL_CONFIRMUSE = c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE),
+ COL_LIFETIME = c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME);
+
+ while (c.moveToNext()) {
+ PubkeyBean pubkey = new PubkeyBean();
+
+ pubkey.setId(c.getLong(COL_ID));
+ pubkey.setNickname(c.getString(COL_NICKNAME));
+ pubkey.setType(c.getString(COL_TYPE));
+ pubkey.setPrivateKey(c.getBlob(COL_PRIVATE));
+ pubkey.setPublicKey(c.getBlob(COL_PUBLIC));
+ pubkey.setEncrypted(c.getInt(COL_ENCRYPTED) > 0);
+ pubkey.setStartup(c.getInt(COL_STARTUP) > 0);
+ pubkey.setConfirmUse(c.getInt(COL_CONFIRMUSE) > 0);
+ pubkey.setLifetime(c.getInt(COL_LIFETIME));
+
+ pubkeys.add(pubkey);
+ }
+
+ c.close();
+ }
+
+ db.close();
+
+ return pubkeys;
+ }
+
+ /**
+ * @param hostId
+ * @return
+ */
+ public PubkeyBean findPubkeyById(long pubkeyId) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor c = db.query(TABLE_PUBKEYS, null,
+ "_id = ?", new String[] { String.valueOf(pubkeyId) },
+ null, null, null);
+
+ PubkeyBean pubkey = null;
+
+ if (c != null) {
+ if (c.moveToFirst())
+ pubkey = createPubkeyBean(c);
+
+ c.close();
+ }
+
+ db.close();
+
+ return pubkey;
+ }
+
+ private PubkeyBean createPubkeyBean(Cursor c) {
+ PubkeyBean pubkey = new PubkeyBean();
+
+ pubkey.setId(c.getLong(c.getColumnIndexOrThrow("_id")));
+ pubkey.setNickname(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME)));
+ pubkey.setType(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE)));
+ pubkey.setPrivateKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE)));
+ pubkey.setPublicKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC)));
+ pubkey.setEncrypted(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED)) > 0);
+ pubkey.setStartup(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP)) > 0);
+ pubkey.setConfirmUse(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE)) > 0);
+ pubkey.setLifetime(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME)));
+
+ return pubkey;
+ }
+
+ /**
+ * Pull all values for a given column as a list of Strings, probably for use
+ * in a ListPreference. Sorted by <code>_id</code> ascending.
+ */
+ public List<CharSequence> allValues(String column) {
+ List<CharSequence> list = new LinkedList<CharSequence>();
+
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id", column },
+ null, null, null, null, "_id ASC");
+
+ if (c != null) {
+ int COL = c.getColumnIndexOrThrow(column);
+
+ while (c.moveToNext())
+ list.add(c.getString(COL));
+
+ c.close();
+ }
+
+ db.close();
+
+ return list;
+ }
+
+ public String getNickname(long id) {
+ String nickname = null;
+
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id",
+ FIELD_PUBKEY_NICKNAME }, "_id = ?",
+ new String[] { Long.toString(id) }, null, null, null);
+
+ if (c != null) {
+ if (c.moveToFirst())
+ nickname = c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME));
+
+ c.close();
+ }
+
+ db.close();
+
+ return nickname;
+ }
+
+/*
+ public void setOnStart(long id, boolean onStart) {
+
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_PUBKEY_STARTUP, onStart ? 1 : 0);
+
+ db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { Long.toString(id) });
+
+ }
+
+ public boolean changePassword(long id, String oldPassword, String newPassword) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException {
+ SQLiteDatabase db = this.getWritableDatabase();
+
+ Cursor c = db.query(TABLE_PUBKEYS, new String[] { FIELD_PUBKEY_TYPE,
+ FIELD_PUBKEY_PRIVATE, FIELD_PUBKEY_ENCRYPTED },
+ "_id = ?", new String[] { String.valueOf(id) },
+ null, null, null);
+
+ if (!c.moveToFirst())
+ return false;
+
+ String keyType = c.getString(0);
+ byte[] encPriv = c.getBlob(1);
+ c.close();
+
+ PrivateKey priv;
+ try {
+ priv = PubkeyUtils.decodePrivate(encPriv, keyType, oldPassword);
+ } catch (InvalidKeyException e) {
+ return false;
+ } catch (BadPaddingException e) {
+ return false;
+ } catch (InvalidKeySpecException e) {
+ return false;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_PUBKEY_PRIVATE, PubkeyUtils.getEncodedPrivate(priv, newPassword));
+ values.put(FIELD_PUBKEY_ENCRYPTED, newPassword.length() > 0 ? 1 : 0);
+ db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(id) });
+
+ return true;
+ }
+ */
+
+ /**
+ * @param pubkey
+ */
+ public PubkeyBean savePubkey(PubkeyBean pubkey) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ boolean success = false;
+
+ ContentValues values = pubkey.getValues();
+
+ if (pubkey.getId() > 0) {
+ values.remove("_id");
+ if (db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(pubkey.getId()) }) > 0)
+ success = true;
+ }
+
+ if (!success) {
+ long id = db.insert(TABLE_PUBKEYS, null, pubkey.getValues());
+ pubkey.setId(id);
+ }
+
+ db.close();
+
+ return pubkey;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/PubkeyUtils.java b/app/src/main/java/org/connectbot/util/PubkeyUtils.java
new file mode 100644
index 0000000..e7922bd
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/PubkeyUtils.java
@@ -0,0 +1,352 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.keyczar.jce.EcCore;
+
+import android.util.Log;
+
+import com.trilead.ssh2.crypto.Base64;
+import com.trilead.ssh2.crypto.SimpleDERReader;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.ECDSASHA2Verify;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+
+public class PubkeyUtils {
+ private static final String TAG = "PubkeyUtils";
+
+ public static final String PKCS8_START = "-----BEGIN PRIVATE KEY-----";
+ public static final String PKCS8_END = "-----END PRIVATE KEY-----";
+
+ // Size in bytes of salt to use.
+ private static final int SALT_SIZE = 8;
+
+ // Number of iterations for password hashing. PKCS#5 recommends 1000
+ private static final int ITERATIONS = 1000;
+
+ // Cannot be instantiated
+ private PubkeyUtils() {
+ }
+
+ public static String formatKey(Key key){
+ String algo = key.getAlgorithm();
+ String fmt = key.getFormat();
+ byte[] encoded = key.getEncoded();
+ return "Key[algorithm=" + algo + ", format=" + fmt +
+ ", bytes=" + encoded.length + "]";
+ }
+
+ public static byte[] sha256(byte[] data) throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance("SHA-256").digest(data);
+ }
+
+ public static byte[] cipher(int mode, byte[] data, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ SecretKeySpec secretKeySpec = new SecretKeySpec(sha256(secret), "AES");
+ Cipher c = Cipher.getInstance("AES");
+ c.init(mode, secretKeySpec);
+ return c.doFinal(data);
+ }
+
+ public static byte[] encrypt(byte[] cleartext, String secret) throws Exception {
+ byte[] salt = new byte[SALT_SIZE];
+
+ byte[] ciphertext = Encryptor.encrypt(salt, ITERATIONS, secret, cleartext);
+
+ byte[] complete = new byte[salt.length + ciphertext.length];
+
+ System.arraycopy(salt, 0, complete, 0, salt.length);
+ System.arraycopy(ciphertext, 0, complete, salt.length, ciphertext.length);
+
+ Arrays.fill(salt, (byte) 0x00);
+ Arrays.fill(ciphertext, (byte) 0x00);
+
+ return complete;
+ }
+
+ public static byte[] decrypt(byte[] saltAndCiphertext, String secret) throws Exception {
+ try {
+ byte[] salt = new byte[SALT_SIZE];
+ byte[] ciphertext = new byte[saltAndCiphertext.length - salt.length];
+
+ System.arraycopy(saltAndCiphertext, 0, salt, 0, salt.length);
+ System.arraycopy(saltAndCiphertext, salt.length, ciphertext, 0, ciphertext.length);
+
+ return Encryptor.decrypt(salt, ITERATIONS, secret, ciphertext);
+ } catch (Exception e) {
+ Log.d("decrypt", "Could not decrypt with new method", e);
+ // We might be using the old encryption method.
+ return cipher(Cipher.DECRYPT_MODE, saltAndCiphertext, secret.getBytes());
+ }
+ }
+
+ public static byte[] getEncodedPrivate(PrivateKey pk, String secret) throws Exception {
+ final byte[] encoded = pk.getEncoded();
+ if (secret == null || secret.length() == 0) {
+ return encoded;
+ }
+ return encrypt(pk.getEncoded(), secret);
+ }
+
+ public static PrivateKey decodePrivate(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded);
+ KeyFactory kf = KeyFactory.getInstance(keyType);
+ return kf.generatePrivate(privKeySpec);
+ }
+
+ public static PrivateKey decodePrivate(byte[] encoded, String keyType, String secret) throws Exception {
+ if (secret != null && secret.length() > 0)
+ return decodePrivate(decrypt(encoded, secret), keyType);
+ else
+ return decodePrivate(encoded, keyType);
+ }
+
+ public static PublicKey decodePublic(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encoded);
+ KeyFactory kf = KeyFactory.getInstance(keyType);
+ return kf.generatePublic(pubKeySpec);
+ }
+
+ static String getAlgorithmForOid(String oid) throws NoSuchAlgorithmException {
+ if ("1.2.840.10045.2.1".equals(oid)) {
+ return "EC";
+ } else if ("1.2.840.113549.1.1.1".equals(oid)) {
+ return "RSA";
+ } else if ("1.2.840.10040.4.1".equals(oid)) {
+ return "DSA";
+ } else {
+ throw new NoSuchAlgorithmException("Unknown algorithm OID " + oid);
+ }
+ }
+
+ static String getOidFromPkcs8Encoded(byte[] encoded) throws NoSuchAlgorithmException {
+ if (encoded == null) {
+ throw new NoSuchAlgorithmException("encoding is null");
+ }
+
+ try {
+ SimpleDERReader reader = new SimpleDERReader(encoded);
+ reader.resetInput(reader.readSequenceAsByteArray());
+ reader.readInt();
+ reader.resetInput(reader.readSequenceAsByteArray());
+ return reader.readOid();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not read OID", e);
+ throw new NoSuchAlgorithmException("Could not read key", e);
+ }
+ }
+
+ public static KeyPair recoverKeyPair(byte[] encoded) throws NoSuchAlgorithmException,
+ InvalidKeySpecException {
+ final String algo = getAlgorithmForOid(getOidFromPkcs8Encoded(encoded));
+
+ final KeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded);
+
+ final KeyFactory kf = KeyFactory.getInstance(algo);
+ final PrivateKey priv = kf.generatePrivate(privKeySpec);
+
+ return new KeyPair(recoverPublicKey(kf, priv), priv);
+ }
+
+ static PublicKey recoverPublicKey(KeyFactory kf, PrivateKey priv)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (priv instanceof RSAPrivateCrtKey) {
+ RSAPrivateCrtKey rsaPriv = (RSAPrivateCrtKey) priv;
+ return kf.generatePublic(new RSAPublicKeySpec(rsaPriv.getModulus(), rsaPriv
+ .getPublicExponent()));
+ } else if (priv instanceof DSAPrivateKey) {
+ DSAPrivateKey dsaPriv = (DSAPrivateKey) priv;
+ DSAParams params = dsaPriv.getParams();
+
+ // Calculate public key Y
+ BigInteger y = params.getG().modPow(dsaPriv.getX(), params.getP());
+
+ return kf.generatePublic(new DSAPublicKeySpec(y, params.getP(), params.getQ(), params
+ .getG()));
+ } else if (priv instanceof ECPrivateKey) {
+ ECPrivateKey ecPriv = (ECPrivateKey) priv;
+ ECParameterSpec params = ecPriv.getParams();
+
+ // Calculate public key Y
+ ECPoint generator = params.getGenerator();
+ BigInteger[] wCoords = EcCore.multiplyPointA(new BigInteger[] { generator.getAffineX(),
+ generator.getAffineY() }, ecPriv.getS(), params);
+ ECPoint w = new ECPoint(wCoords[0], wCoords[1]);
+
+ return kf.generatePublic(new ECPublicKeySpec(w, params));
+ } else {
+ throw new NoSuchAlgorithmException("Key type must be RSA, DSA, or EC");
+ }
+ }
+
+ /*
+ * OpenSSH compatibility methods
+ */
+
+ public static String convertToOpenSSHFormat(PublicKey pk, String origNickname) throws IOException, InvalidKeyException {
+ String nickname = origNickname;
+ if (nickname == null)
+ nickname = "connectbot@android";
+
+ if (pk instanceof RSAPublicKey) {
+ String data = "ssh-rsa ";
+ data += String.valueOf(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pk)));
+ return data + " " + nickname;
+ } else if (pk instanceof DSAPublicKey) {
+ String data = "ssh-dss ";
+ data += String.valueOf(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pk)));
+ return data + " " + nickname;
+ } else if (pk instanceof ECPublicKey) {
+ ECPublicKey ecPub = (ECPublicKey) pk;
+ String keyType = ECDSASHA2Verify.getCurveName(ecPub.getParams().getCurve().getField().getFieldSize());
+ String keyData = String.valueOf(Base64.encode(ECDSASHA2Verify.encodeSSHECDSAPublicKey(ecPub)));
+ return ECDSASHA2Verify.ECDSA_SHA2_PREFIX + keyType + " " + keyData + " " + nickname;
+ }
+
+ throw new InvalidKeyException("Unknown key type");
+ }
+
+ /*
+ * OpenSSH compatibility methods
+ */
+
+ /**
+ * @param trileadKey
+ * @return OpenSSH-encoded pubkey
+ */
+ public static byte[] extractOpenSSHPublic(KeyPair pair) {
+ try {
+ PublicKey pubKey = pair.getPublic();
+ if (pubKey instanceof RSAPublicKey) {
+ return RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pair.getPublic());
+ } else if (pubKey instanceof DSAPublicKey) {
+ return DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pair.getPublic());
+ } else if (pubKey instanceof ECPublicKey) {
+ return ECDSASHA2Verify.encodeSSHECDSAPublicKey((ECPublicKey) pair.getPublic());
+ } else {
+ return null;
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static String exportPEM(PrivateKey key, String secret) throws NoSuchAlgorithmException, InvalidParameterSpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, InvalidKeySpecException, IllegalBlockSizeException, IOException {
+ StringBuilder sb = new StringBuilder();
+
+ byte[] data = key.getEncoded();
+
+ sb.append(PKCS8_START);
+ sb.append('\n');
+
+ if (secret != null) {
+ byte[] salt = new byte[8];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(salt);
+
+ PBEParameterSpec defParams = new PBEParameterSpec(salt, 1);
+ AlgorithmParameters params = AlgorithmParameters.getInstance(key.getAlgorithm());
+
+ params.init(defParams);
+
+ PBEKeySpec pbeSpec = new PBEKeySpec(secret.toCharArray());
+
+ SecretKeyFactory keyFact = SecretKeyFactory.getInstance(key.getAlgorithm());
+ Cipher cipher = Cipher.getInstance(key.getAlgorithm());
+ cipher.init(Cipher.WRAP_MODE, keyFact.generateSecret(pbeSpec), params);
+
+ byte[] wrappedKey = cipher.wrap(key);
+
+ EncryptedPrivateKeyInfo pinfo = new EncryptedPrivateKeyInfo(params, wrappedKey);
+
+ data = pinfo.getEncoded();
+
+ sb.append("Proc-Type: 4,ENCRYPTED\n");
+ sb.append("DEK-Info: DES-EDE3-CBC,");
+ sb.append(encodeHex(salt));
+ sb.append("\n\n");
+ }
+
+ int i = sb.length();
+ sb.append(Base64.encode(data));
+ for (i += 63; i < sb.length(); i += 64) {
+ sb.insert(i, "\n");
+ }
+
+ sb.append('\n');
+ sb.append(PKCS8_END);
+ sb.append('\n');
+
+ return sb.toString();
+ }
+
+ private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6',
+ '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ protected static String encodeHex(byte[] bytes) {
+ final char[] hex = new char[bytes.length * 2];
+
+ int i = 0;
+ for (byte b : bytes) {
+ hex[i++] = HEX_DIGITS[(b >> 4) & 0x0f];
+ hex[i++] = HEX_DIGITS[b & 0x0f];
+ }
+
+ return String.valueOf(hex);
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java
new file mode 100644
index 0000000..abdd991
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java
@@ -0,0 +1,133 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class RobustSQLiteOpenHelper extends SQLiteOpenHelper {
+ private static List<String> mTableNames = new LinkedList<String>();
+ private static List<String> mIndexNames = new LinkedList<String>();
+
+ public RobustSQLiteOpenHelper(Context context, String name,
+ CursorFactory factory, int version) {
+ super(context, name, factory, version);
+ }
+
+ protected static void addTableName(String tableName) {
+ mTableNames.add(tableName);
+ }
+
+ protected static void addIndexName(String indexName) {
+ mIndexNames.add(indexName);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ dropAllTables(db);
+ }
+
+ @Override
+ public final void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ onRobustUpgrade(db, oldVersion, newVersion);
+ } catch (SQLiteException e) {
+ // The database has entered an unknown state. Try to recover.
+ try {
+ regenerateTables(db);
+ } catch (SQLiteException e2) {
+ dropAndCreateTables(db);
+ }
+ }
+ }
+
+ public abstract void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException;
+
+ private void regenerateTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "OLD_");
+
+ for (String tableName : mTableNames)
+ db.execSQL("ALTER TABLE " + tableName + " RENAME TO OLD_"
+ + tableName);
+
+ onCreate(db);
+
+ for (String tableName : mTableNames)
+ repopulateTable(db, tableName);
+
+ dropAllTablesWithPrefix(db, "OLD_");
+ }
+
+ private void repopulateTable(SQLiteDatabase db, String tableName) {
+ String columns = getTableColumnNames(db, tableName);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("INSERT INTO ")
+ .append(tableName)
+ .append(" (")
+ .append(columns)
+ .append(") SELECT ")
+ .append(columns)
+ .append(" FROM OLD_")
+ .append(tableName);
+
+ String sql = sb.toString();
+ db.execSQL(sql);
+ }
+
+ private String getTableColumnNames(SQLiteDatabase db, String tableName) {
+ StringBuilder sb = new StringBuilder();
+
+ Cursor fields = db.rawQuery("PRAGMA table_info(" + tableName + ")", null);
+ while (fields.moveToNext()) {
+ if (!fields.isFirst())
+ sb.append(", ");
+ sb.append(fields.getString(1));
+ }
+ fields.close();
+
+ return sb.toString();
+ }
+
+ private void dropAndCreateTables(SQLiteDatabase db) {
+ dropAllTables(db);
+ onCreate(db);
+ }
+
+ private void dropAllTablesWithPrefix(SQLiteDatabase db, String prefix) {
+ for (String indexName : mIndexNames)
+ db.execSQL("DROP INDEX IF EXISTS " + prefix + indexName);
+ for (String tableName : mTableNames)
+ db.execSQL("DROP TABLE IF EXISTS " + prefix + tableName);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "");
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java
new file mode 100644
index 0000000..2c01b30
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * 090408
+ * Keith Wiley
+ * kwiley@keithwiley.com
+ * http://keithwiley.com
+ *
+ * UberColorPickerDialog v1.1
+ *
+ * This color picker was implemented as a (significant) extension of the
+ * ColorPickerDialog class provided in the Android API Demos. You are free
+ * to drop it unchanged into your own projects or to modify it as you see
+ * fit. I would appreciate it if this comment block were let intact,
+ * merely for credit's sake.
+ *
+ * Enjoy!
+ */
+
+package org.connectbot.util;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ComposeShader;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * UberColorPickerDialog is a seriously enhanced version of the UberColorPickerDialog
+ * class provided in the Android API Demos.<p>
+ *
+ * NOTE (from Kenny Root): This is a VERY slimmed down version custom for ConnectBot.
+ * Visit Keith's site for the full version at the URL listed in the author line.<p>
+ *
+ * @author Keith Wiley, kwiley@keithwiley.com, http://keithwiley.com
+ */
+public class UberColorPickerDialog extends Dialog {
+ private final OnColorChangedListener mListener;
+ private final int mInitialColor;
+
+ /**
+ * Callback to the creator of the dialog, informing the creator of a new color and notifying that the dialog is about to dismiss.
+ */
+ public interface OnColorChangedListener {
+ void colorChanged(int color);
+ }
+
+ /**
+ * Ctor
+ * @param context
+ * @param listener
+ * @param initialColor
+ * @param showTitle If true, a title is shown across the top of the dialog. If false a toast is shown instead.
+ */
+ public UberColorPickerDialog(Context context,
+ OnColorChangedListener listener,
+ int initialColor) {
+ super(context);
+
+ mListener = listener;
+ mInitialColor = initialColor;
+ }
+
+ /**
+ * Activity entry point
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ OnColorChangedListener l = new OnColorChangedListener() {
+ public void colorChanged(int color) {
+ mListener.colorChanged(color);
+ dismiss();
+ }
+ };
+
+ DisplayMetrics dm = new DisplayMetrics();
+ getWindow().getWindowManager().getDefaultDisplay().getMetrics(dm);
+ int screenWidth = dm.widthPixels;
+ int screenHeight = dm.heightPixels;
+
+ setTitle("Pick a color (try the trackball)");
+
+ try {
+ setContentView(new ColorPickerView(getContext(), l, screenWidth, screenHeight, mInitialColor));
+ }
+ catch (Exception e) {
+ //There is currently only one kind of ctor exception, that where no methods are enabled.
+ dismiss(); //This doesn't work! The dialog is still shown (its title at least, the layout is empty from the exception being thrown). <sigh>
+ }
+ }
+
+ /**
+ * ColorPickerView is the meat of this color picker (as opposed to the enclosing class).
+ * All the heavy lifting is done directly by this View subclass.
+ * <P>
+ * You can enable/disable whichever color chooser methods you want by modifying the ENABLED_METHODS switches. They *should*
+ * do all the work required to properly enable/disable methods without losing track of what goes with what and what maps to what.
+ * <P>
+ * If you add a new color chooser method, do a text search for "NEW_METHOD_WORK_NEEDED_HERE". That tag indicates all
+ * the locations in the code that will have to be amended in order to properly add a new color chooser method.
+ * I highly recommend adding new methods to the end of the list. If you want to try to reorder the list, you're on your own.
+ */
+ private static class ColorPickerView extends View {
+ private static int SWATCH_WIDTH = 95;
+ private static final int SWATCH_HEIGHT = 60;
+
+ private static int PALETTE_POS_X = 0;
+ private static int PALETTE_POS_Y = SWATCH_HEIGHT;
+ private static final int PALETTE_DIM = SWATCH_WIDTH * 2;
+ private static final int PALETTE_RADIUS = PALETTE_DIM / 2;
+ private static final int PALETTE_CENTER_X = PALETTE_RADIUS;
+ private static final int PALETTE_CENTER_Y = PALETTE_RADIUS;
+
+ private static final int SLIDER_THICKNESS = 40;
+
+ private static int VIEW_DIM_X = PALETTE_DIM;
+ private static int VIEW_DIM_Y = SWATCH_HEIGHT;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ private static final int METHOD_HS_V_PALETTE = 0;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add a new entry to the list for each controller in the new method
+ private static final int TRACKED_NONE = -1; //No object on screen is currently being tracked
+ private static final int TRACK_SWATCH_OLD = 10;
+ private static final int TRACK_SWATCH_NEW = 11;
+ private static final int TRACK_HS_PALETTE = 30;
+ private static final int TRACK_VER_VALUE_SLIDER = 31;
+
+ private static final int TEXT_SIZE = 12;
+ private static int[] TEXT_HSV_POS = new int[2];
+ private static int[] TEXT_RGB_POS = new int[2];
+ private static int[] TEXT_YUV_POS = new int[2];
+ private static int[] TEXT_HEX_POS = new int[2];
+
+ private static final float PI = 3.141592653589793f;
+
+ private int mMethod = METHOD_HS_V_PALETTE;
+ private int mTracking = TRACKED_NONE; //What object on screen is currently being tracked for movement
+
+ //Zillions of persistant Paint objecs for drawing the View
+
+ private Paint mSwatchOld, mSwatchNew;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the palettes of the new method's UI controllers
+ private Paint mOvalHueSat;
+
+ private Bitmap mVerSliderBM;
+ private Canvas mVerSliderCv;
+
+ private Bitmap[] mHorSlidersBM = new Bitmap[3];
+ private Canvas[] mHorSlidersCv = new Canvas[3];
+
+ private Paint mValDimmer;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the icon for the new method
+ private Paint mOvalHueSatSmall;
+
+ private Paint mPosMarker;
+ private Paint mText;
+
+ private Rect mOldSwatchRect = new Rect();
+ private Rect mNewSwatchRect = new Rect();
+ private Rect mPaletteRect = new Rect();
+ private Rect mVerSliderRect = new Rect();
+
+ private int[] mSpectrumColorsRev;
+ private int mOriginalColor = 0; //The color passed in at the beginning, which can be reverted to at any time by tapping the old swatch.
+ private float[] mHSV = new float[3];
+ private int[] mRGB = new int[3];
+ private float[] mYUV = new float[3];
+ private String mHexStr = "";
+ private boolean mHSVenabled = true; //Only true if an HSV method is enabled
+ private boolean mRGBenabled = true; //Only true if an RGB method is enabled
+ private boolean mYUVenabled = true; //Only true if a YUV method is enabled
+ private boolean mHexenabled = true; //Only true if an RGB method is enabled
+ private int[] mCoord = new int[3]; //For drawing slider/palette markers
+ private int mFocusedControl = -1; //Which control receives trackball events.
+ private OnColorChangedListener mListener;
+
+ /**
+ * Ctor.
+ * @param c
+ * @param l
+ * @param width Used to determine orientation and adjust layout accordingly
+ * @param height Used to determine orientation and adjust layout accordingly
+ * @param color The initial color
+ * @throws Exception
+ */
+ ColorPickerView(Context c, OnColorChangedListener l, int width, int height, int color)
+ throws Exception {
+ super(c);
+
+ //We need to make the dialog focusable to retrieve trackball events.
+ setFocusable(true);
+
+ mListener = l;
+
+ mOriginalColor = color;
+
+ Color.colorToHSV(color, mHSV);
+
+ updateAllFromHSV();
+
+ //Setup the layout based on whether this is a portrait or landscape orientation.
+ if (width <= height) { //Portrait layout
+ SWATCH_WIDTH = (PALETTE_DIM + SLIDER_THICKNESS) / 2;
+
+ PALETTE_POS_X = 0;
+ PALETTE_POS_Y = TEXT_SIZE * 4 + SWATCH_HEIGHT;
+
+ //Set more rects, lots of rects
+ mOldSwatchRect.set(0, TEXT_SIZE * 4, SWATCH_WIDTH, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+ mNewSwatchRect.set(SWATCH_WIDTH, TEXT_SIZE * 4, SWATCH_WIDTH * 2, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+ mPaletteRect.set(0, PALETTE_POS_Y, PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+ mVerSliderRect.set(PALETTE_DIM, PALETTE_POS_Y, PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+ TEXT_HSV_POS[0] = 3;
+ TEXT_HSV_POS[1] = 0;
+ TEXT_RGB_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_RGB_POS[1] = TEXT_HSV_POS[1];
+ TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 100;
+ TEXT_YUV_POS[1] = TEXT_HSV_POS[1];
+ TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 150;
+ TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+ VIEW_DIM_X = PALETTE_DIM + SLIDER_THICKNESS;
+ VIEW_DIM_Y = SWATCH_HEIGHT + PALETTE_DIM + TEXT_SIZE * 4;
+ }
+ else { //Landscape layout
+ SWATCH_WIDTH = 110;
+
+ PALETTE_POS_X = SWATCH_WIDTH;
+ PALETTE_POS_Y = 0;
+
+ //Set more rects, lots of rects
+ mOldSwatchRect.set(0, TEXT_SIZE * 7, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT);
+ mNewSwatchRect.set(0, TEXT_SIZE * 7 + SWATCH_HEIGHT, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT * 2);
+ mPaletteRect.set(SWATCH_WIDTH, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+ mVerSliderRect.set(SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+ TEXT_HSV_POS[0] = 3;
+ TEXT_HSV_POS[1] = 0;
+ TEXT_RGB_POS[0] = TEXT_HSV_POS[0];
+ TEXT_RGB_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+ TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_YUV_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+ TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 50;
+ TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+ VIEW_DIM_X = PALETTE_POS_X + PALETTE_DIM + SLIDER_THICKNESS;
+ VIEW_DIM_Y = Math.max(mNewSwatchRect.bottom, PALETTE_DIM);
+ }
+
+ //Rainbows make everybody happy!
+ mSpectrumColorsRev = new int[] {
+ 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF,
+ 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000,
+ };
+
+ //Setup all the Paint and Shader objects. There are lots of them!
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the palettes of the new method's UI controllers
+
+ mSwatchOld = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSwatchOld.setStyle(Paint.Style.FILL);
+ mSwatchOld.setColor(Color.HSVToColor(mHSV));
+
+ mSwatchNew = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSwatchNew.setStyle(Paint.Style.FILL);
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ Shader shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+ Shader shaderB = new RadialGradient(0, 0, PALETTE_CENTER_X, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+ Shader shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+ mOvalHueSat = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mOvalHueSat.setShader(shader);
+ mOvalHueSat.setStyle(Paint.Style.FILL);
+ mOvalHueSat.setDither(true);
+
+ mVerSliderBM = Bitmap.createBitmap(SLIDER_THICKNESS, PALETTE_DIM, Bitmap.Config.RGB_565);
+ mVerSliderCv = new Canvas(mVerSliderBM);
+
+ for (int i = 0; i < 3; i++) {
+ mHorSlidersBM[i] = Bitmap.createBitmap(PALETTE_DIM, SLIDER_THICKNESS, Bitmap.Config.RGB_565);
+ mHorSlidersCv[i] = new Canvas(mHorSlidersBM[i]);
+ }
+
+ mValDimmer = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mValDimmer.setStyle(Paint.Style.FILL);
+ mValDimmer.setDither(true);
+ mValDimmer.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+
+ //Whew, we're done making the big Paints and Shaders for the swatches, palettes, and sliders.
+ //Now we need to make the Paints and Shaders that will draw the little method icons in the method selector list.
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //Add Paints to represent the icon for the new method
+
+ shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+ shaderB = new RadialGradient(0, 0, PALETTE_DIM / 2, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+ shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+ mOvalHueSatSmall = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mOvalHueSatSmall.setShader(shader);
+ mOvalHueSatSmall.setStyle(Paint.Style.FILL);
+
+ //Make a simple stroking Paint for drawing markers and borders and stuff like that.
+ mPosMarker = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPosMarker.setStyle(Paint.Style.STROKE);
+ mPosMarker.setStrokeWidth(2);
+
+ //Make a basic text Paint.
+ mText = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mText.setTextSize(TEXT_SIZE);
+ mText.setColor(Color.WHITE);
+
+ //Kickstart
+ initUI();
+ }
+
+ /**
+ * Draw the entire view (the entire dialog).
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ //Draw the old and new swatches
+ drawSwatches(canvas);
+
+ //Write the text
+ writeColorParams(canvas);
+
+ //Draw the palette and sliders (the UI)
+ if (mMethod == METHOD_HS_V_PALETTE)
+ drawHSV1Palette(canvas);
+ }
+
+ /**
+ * Draw the old and new swatches.
+ * @param canvas
+ */
+ private void drawSwatches(Canvas canvas) {
+ float[] hsv = new float[3];
+
+ mText.setTextSize(16);
+
+ //Draw the original swatch
+ canvas.drawRect(mOldSwatchRect, mSwatchOld);
+ Color.colorToHSV(mOriginalColor, hsv);
+ //if (UberColorPickerDialog.isGray(mColor)) //Don't need this right here, but imp't to note
+ // hsv[1] = 0;
+ if (hsv[2] > .5)
+ mText.setColor(Color.BLACK);
+ canvas.drawText("Revert", mOldSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Revert") / 2, mOldSwatchRect.top + 16, mText);
+ mText.setColor(Color.WHITE);
+
+ //Draw the new swatch
+ canvas.drawRect(mNewSwatchRect, mSwatchNew);
+ if (mHSV[2] > .5)
+ mText.setColor(Color.BLACK);
+ canvas.drawText("Accept", mNewSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Accept") / 2, mNewSwatchRect.top + 16, mText);
+ mText.setColor(Color.WHITE);
+
+ mText.setTextSize(TEXT_SIZE);
+ }
+
+ /**
+ * Write the color parametes (HSV, RGB, YUV, Hex, etc.).
+ * @param canvas
+ */
+ private void writeColorParams(Canvas canvas) {
+ if (mHSVenabled) {
+ canvas.drawText("H: " + Integer.toString((int)(mHSV[0] / 360.0f * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("S: " + Integer.toString((int)(mHSV[1] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("V: " + Integer.toString((int)(mHSV[2] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mRGBenabled) {
+ canvas.drawText("R: " + mRGB[0], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("G: " + mRGB[1], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("B: " + mRGB[2], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mYUVenabled) {
+ canvas.drawText("Y: " + Integer.toString((int)(mYUV[0] * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE, mText);
+ canvas.drawText("U: " + Integer.toString((int)((mYUV[1] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 2, mText);
+ canvas.drawText("V: " + Integer.toString((int)((mYUV[2] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 3, mText);
+ }
+
+ if (mHexenabled)
+ canvas.drawText("#" + mHexStr, TEXT_HEX_POS[0], TEXT_HEX_POS[1] + TEXT_SIZE, mText);
+ }
+
+ /**
+ * Place a small circle on the 2D palette to indicate the current values.
+ * @param canvas
+ * @param markerPosX
+ * @param markerPosY
+ */
+ private void mark2DPalette(Canvas canvas, int markerPosX, int markerPosY) {
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawOval(new RectF(markerPosX - 5, markerPosY - 5, markerPosX + 5, markerPosY + 5), mPosMarker);
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawOval(new RectF(markerPosX - 3, markerPosY - 3, markerPosX + 3, markerPosY + 3), mPosMarker);
+ }
+
+ /**
+ * Draw a line across the slider to indicate its current value.
+ * @param canvas
+ * @param markerPos
+ */
+ private void markVerSlider(Canvas canvas, int markerPos) {
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawRect(new Rect(0, markerPos - 2, SLIDER_THICKNESS, markerPos + 3), mPosMarker);
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawRect(new Rect(0, markerPos, SLIDER_THICKNESS, markerPos + 1), mPosMarker);
+ }
+
+ /**
+ * Frame the slider to indicate that it has trackball focus.
+ * @param canvas
+ */
+ private void hilightFocusedVerSlider(Canvas canvas) {
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawRect(new Rect(0, 0, SLIDER_THICKNESS, PALETTE_DIM), mPosMarker);
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawRect(new Rect(2, 2, SLIDER_THICKNESS - 2, PALETTE_DIM - 2), mPosMarker);
+ }
+
+ /**
+ * Frame the 2D palette to indicate that it has trackball focus.
+ * @param canvas
+ */
+ private void hilightFocusedOvalPalette(Canvas canvas) {
+ mPosMarker.setColor(Color.WHITE);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mPosMarker);
+ mPosMarker.setColor(Color.BLACK);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS + 2, -PALETTE_RADIUS + 2, PALETTE_RADIUS - 2, PALETTE_RADIUS - 2), mPosMarker);
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate the basic draw functions here. Use the 2D palette or 1D sliders as templates for the new method.
+ /**
+ * Draw the UI for HSV with angular H and radial S combined in 2D and a 1D V slider.
+ * @param canvas
+ */
+ private void drawHSV1Palette(Canvas canvas) {
+ canvas.save();
+
+ canvas.translate(PALETTE_POS_X, PALETTE_POS_Y);
+
+ //Draw the 2D palette
+ canvas.translate(PALETTE_CENTER_X, PALETTE_CENTER_Y);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mOvalHueSat);
+ canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mValDimmer);
+ if (mFocusedControl == 0)
+ hilightFocusedOvalPalette(canvas);
+ mark2DPalette(canvas, mCoord[0], mCoord[1]);
+ canvas.translate(-PALETTE_CENTER_X, -PALETTE_CENTER_Y);
+
+ //Draw the 1D slider
+ canvas.translate(PALETTE_DIM, 0);
+ canvas.drawBitmap(mVerSliderBM, 0, 0, null);
+ if (mFocusedControl == 1)
+ hilightFocusedVerSlider(canvas);
+ markVerSlider(canvas, mCoord[2]);
+
+ canvas.restore();
+ }
+
+ /**
+ * Initialize the current color chooser's UI (set its color parameters and set its palette and slider values accordingly).
+ */
+ private void initUI() {
+ initHSV1Palette();
+
+ //Focus on the first controller (arbitrary).
+ mFocusedControl = 0;
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last init function shown below
+ /**
+ * Initialize a color chooser.
+ */
+ private void initHSV1Palette() {
+ setOvalValDimmer();
+ setVerValSlider();
+
+ float angle = 2*PI - mHSV[0] / (180 / 3.1415927f);
+ float radius = mHSV[1] * PALETTE_RADIUS;
+ mCoord[0] = (int)(Math.cos(angle) * radius);
+ mCoord[1] = (int)(Math.sin(angle) * radius);
+
+ mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the set functions below, one per UI controller in the new method
+ /**
+ * Adjust a Paint which, when painted, dims its underlying object to show the effects of varying value (brightness).
+ */
+ private void setOvalValDimmer() {
+ float[] hsv = new float[3];
+ hsv[0] = mHSV[0];
+ hsv[1] = 0;
+ hsv[2] = mHSV[2];
+ int gray = Color.HSVToColor(hsv);
+ mValDimmer.setColor(gray);
+ }
+
+ /**
+ * Create a linear gradient shader to show variations in value.
+ */
+ private void setVerValSlider() {
+ float[] hsv = new float[3];
+ hsv[0] = mHSV[0];
+ hsv[1] = mHSV[1];
+ hsv[2] = 1;
+ int col = Color.HSVToColor(hsv);
+
+ int colors[] = new int[2];
+ colors[0] = col;
+ colors[1] = 0xFF000000;
+ GradientDrawable gradDraw = new GradientDrawable(Orientation.TOP_BOTTOM, colors);
+ gradDraw.setDither(true);
+ gradDraw.setLevel(10000);
+ gradDraw.setBounds(0, 0, SLIDER_THICKNESS, PALETTE_DIM);
+ gradDraw.draw(mVerSliderCv);
+ }
+
+ /**
+ * Report the correct tightly bounded dimensions of the view.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(VIEW_DIM_X, VIEW_DIM_Y);
+ }
+
+ /**
+ * Wrap Math.round(). I'm not a Java expert. Is this the only way to avoid writing "(int)Math.round" everywhere?
+ * @param x
+ * @return
+ */
+ private int round(double x) {
+ return (int)Math.round(x);
+ }
+
+ /**
+ * Limit a value to the range [0,1].
+ * @param n
+ * @return
+ */
+ private float pinToUnit(float n) {
+ if (n < 0) {
+ n = 0;
+ } else if (n > 1) {
+ n = 1;
+ }
+ return n;
+ }
+
+ /**
+ * Limit a value to the range [0,max].
+ * @param n
+ * @param max
+ * @return
+ */
+ private float pin(float n, float max) {
+ if (n < 0) {
+ n = 0;
+ } else if (n > max) {
+ n = max;
+ }
+ return n;
+ }
+
+ /**
+ * Limit a value to the range [min,max].
+ * @param n
+ * @param min
+ * @param max
+ * @return
+ */
+ private float pin(float n, float min, float max) {
+ if (n < min) {
+ n = min;
+ } else if (n > max) {
+ n = max;
+ }
+ return n;
+ }
+
+ /**
+ * No clue what this does (some sort of average/mean I presume). It came with the original UberColorPickerDialog
+ * in the API Demos and wasn't documented. I don't feel like spending any time figuring it out, I haven't looked at it at all.
+ * @param s
+ * @param d
+ * @param p
+ * @return
+ */
+ private int ave(int s, int d, float p) {
+ return s + round(p * (d - s));
+ }
+
+ /**
+ * Came with the original UberColorPickerDialog in the API Demos, wasn't documented. I believe it takes an array of
+ * colors and a value in the range [0,1] and interpolates a resulting color in a seemingly predictable manner.
+ * I haven't looked at it at all.
+ * @param colors
+ * @param unit
+ * @return
+ */
+ private int interpColor(int colors[], float unit) {
+ if (unit <= 0) {
+ return colors[0];
+ }
+ if (unit >= 1) {
+ return colors[colors.length - 1];
+ }
+
+ float p = unit * (colors.length - 1);
+ int i = (int)p;
+ p -= i;
+
+ // now p is just the fractional part [0...1) and i is the index
+ int c0 = colors[i];
+ int c1 = colors[i+1];
+ int a = ave(Color.alpha(c0), Color.alpha(c1), p);
+ int r = ave(Color.red(c0), Color.red(c1), p);
+ int g = ave(Color.green(c0), Color.green(c1), p);
+ int b = ave(Color.blue(c0), Color.blue(c1), p);
+
+ return Color.argb(a, r, g, b);
+ }
+
+ /**
+ * A standard point-in-rect routine.
+ * @param x
+ * @param y
+ * @param r
+ * @return true if point x,y is in rect r
+ */
+ public boolean ptInRect(int x, int y, Rect r) {
+ return x > r.left && x < r.right && y > r.top && y < r.bottom;
+ }
+
+ /**
+ * Process trackball events. Used mainly for fine-tuned color adjustment, or alternatively to switch between slider controls.
+ */
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ //A longer event history implies faster trackball movement.
+ //Use it to infer a larger jump and therefore faster palette/slider adjustment.
+ int jump = event.getHistorySize() + 1;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ }
+ break;
+ case MotionEvent.ACTION_MOVE: {
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the appropriate entry in this list,
+ //depending on whether you use 1D or 2D controllers
+ switch (mMethod) {
+ case METHOD_HS_V_PALETTE:
+ if (mFocusedControl == 0) {
+ changeHSPalette(x, y, jump);
+ }
+ else if (mFocusedControl == 1) {
+ if (y < 0)
+ changeSlider(mFocusedControl, true, jump);
+ else if (y > 0)
+ changeSlider(mFocusedControl, false, jump);
+ }
+ break;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP: {
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the appropriate functions below,
+ //one per UI controller in the new method
+ /**
+ * Effect a trackball change to a 2D palette.
+ * @param x -1: negative x change, 0: no x change, +1: positive x change.
+ * @param y -1: negative y change, 0, no y change, +1: positive y change.
+ * @param jump the amount by which to change.
+ */
+ private void changeHSPalette(float x, float y, int jump) {
+ int x2 = 0, y2 = 0;
+ if (x < 0)
+ x2 = -jump;
+ else if (x > 0)
+ x2 = jump;
+ if (y < 0)
+ y2 = -jump;
+ else if (y > 0)
+ y2 = jump;
+
+ mCoord[0] += x2;
+ mCoord[1] += y2;
+
+ if (mCoord[0] < -PALETTE_RADIUS)
+ mCoord[0] = -PALETTE_RADIUS;
+ else if (mCoord[0] > PALETTE_RADIUS)
+ mCoord[0] = PALETTE_RADIUS;
+ if (mCoord[1] < -PALETTE_RADIUS)
+ mCoord[1] = -PALETTE_RADIUS;
+ else if (mCoord[1] > PALETTE_RADIUS)
+ mCoord[1] = PALETTE_RADIUS;
+
+ float radius = (float)java.lang.Math.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]);
+ if (radius > PALETTE_RADIUS)
+ radius = PALETTE_RADIUS;
+
+ float angle = (float)java.lang.Math.atan2(mCoord[1], mCoord[0]);
+ // need to turn angle [-PI ... PI] into unit [0....1]
+ float unit = angle/(2*PI);
+ if (unit < 0) {
+ unit += 1;
+ }
+
+ mCoord[0] = round(Math.cos(angle) * radius);
+ mCoord[1] = round(Math.sin(angle) * radius);
+
+ int c = interpColor(mSpectrumColorsRev, unit);
+ float[] hsv = new float[3];
+ Color.colorToHSV(c, hsv);
+ mHSV[0] = hsv[0];
+ mHSV[1] = radius / PALETTE_RADIUS;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setVerValSlider();
+
+ invalidate();
+ }
+
+ /**
+ * Effect a trackball change to a 1D slider.
+ * @param slider id of the slider to be effected
+ * @param increase true if the change is an increase, false if a decrease
+ * @param jump the amount by which to change in units of the range [0,255]
+ */
+ private void changeSlider(int slider, boolean increase, int jump) {
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //It is only necessary to add an entry here for a new method if the new method uses a 1D slider.
+ //Note, some sliders are horizontal and others are vertical.
+ //They differ a bit, especially in a sign flip on the vertical axis.
+ if (mMethod == METHOD_HS_V_PALETTE) {
+ //slider *must* equal 1
+
+ mHSV[2] += (increase ? jump : -jump) / 256.0f;
+ mHSV[2] = pinToUnit(mHSV[2]);
+ updateAllFromHSV();
+ mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setOvalValDimmer();
+
+ invalidate();
+ }
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateRGBfromHSV() {
+ int color = Color.HSVToColor(mHSV);
+ mRGB[0] = Color.red(color);
+ mRGB[1] = Color.green(color);
+ mRGB[2] = Color.blue(color);
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateYUVfromRGB() {
+ float r = mRGB[0] / 255.0f;
+ float g = mRGB[1] / 255.0f;
+ float b = mRGB[2] / 255.0f;
+
+ ColorMatrix cm = new ColorMatrix();
+ cm.setRGB2YUV();
+ final float[] a = cm.getArray();
+
+ mYUV[0] = a[0] * r + a[1] * g + a[2] * b;
+ mYUV[0] = pinToUnit(mYUV[0]);
+ mYUV[1] = a[5] * r + a[6] * g + a[7] * b;
+ mYUV[1] = pin(mYUV[1], -.5f, .5f);
+ mYUV[2] = a[10] * r + a[11] * g + a[12] * b;
+ mYUV[2] = pin(mYUV[2], -.5f, .5f);
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateHexFromHSV() {
+ //For now, assume 100% opacity
+ mHexStr = Integer.toHexString(Color.HSVToColor(mHSV)).toUpperCase();
+ mHexStr = mHexStr.substring(2, mHexStr.length());
+ }
+
+ /**
+ * Keep all colorspace representations in sync.
+ */
+ private void updateAllFromHSV() {
+ //Update mRGB
+ if (mRGBenabled || mYUVenabled)
+ updateRGBfromHSV();
+
+ //Update mYUV
+ if (mYUVenabled)
+ updateYUVfromRGB();
+
+ //Update mHexStr
+ if (mRGBenabled)
+ updateHexFromHSV();
+ }
+
+ /**
+ * Process touch events: down, move, and up
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ //Generate coordinates which are palette=local with the origin at the upper left of the main 2D palette
+ int y2 = (int)(pin(round(y - PALETTE_POS_Y), PALETTE_DIM));
+
+ //Generate coordinates which are palette-local with the origin at the center of the main 2D palette
+ float circlePinnedX = x - PALETTE_POS_X - PALETTE_CENTER_X;
+ float circlePinnedY = y - PALETTE_POS_Y - PALETTE_CENTER_Y;
+
+ //Is the event in a swatch?
+ boolean inSwatchOld = ptInRect(round(x), round(y), mOldSwatchRect);
+ boolean inSwatchNew = ptInRect(round(x), round(y), mNewSwatchRect);
+
+ //Get the event's distance from the center of the main 2D palette
+ float radius = (float)java.lang.Math.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY);
+
+ //Is the event in a circle-pinned 2D palette?
+ boolean inOvalPalette = radius <= PALETTE_RADIUS;
+
+ //Pin the radius
+ if (radius > PALETTE_RADIUS)
+ radius = PALETTE_RADIUS;
+
+ //Is the event in a vertical slider to the right of the main 2D palette
+ boolean inVerSlider = ptInRect(round(x), round(y), mVerSliderRect);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTracking = TRACKED_NONE;
+
+ if (inSwatchOld)
+ mTracking = TRACK_SWATCH_OLD;
+ else if (inSwatchNew)
+ mTracking = TRACK_SWATCH_NEW;
+
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last entry in this list
+ else if (mMethod == METHOD_HS_V_PALETTE) {
+ if (inOvalPalette) {
+ mTracking = TRACK_HS_PALETTE;
+ mFocusedControl = 0;
+ }
+ else if (inVerSlider) {
+ mTracking = TRACK_VER_VALUE_SLIDER;
+ mFocusedControl = 1;
+ }
+ }
+ case MotionEvent.ACTION_MOVE:
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the entries in this list,
+ //one per UI controller the new method requires.
+ if (mTracking == TRACK_HS_PALETTE) {
+ float angle = (float)java.lang.Math.atan2(circlePinnedY, circlePinnedX);
+ // need to turn angle [-PI ... PI] into unit [0....1]
+ float unit = angle/(2*PI);
+ if (unit < 0) {
+ unit += 1;
+ }
+
+ mCoord[0] = round(Math.cos(angle) * radius);
+ mCoord[1] = round(Math.sin(angle) * radius);
+
+ int c = interpColor(mSpectrumColorsRev, unit);
+ float[] hsv = new float[3];
+ Color.colorToHSV(c, hsv);
+ mHSV[0] = hsv[0];
+ mHSV[1] = radius / PALETTE_RADIUS;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setVerValSlider();
+
+ invalidate();
+ }
+ else if (mTracking == TRACK_VER_VALUE_SLIDER) {
+ if (mCoord[2] != y2) {
+ mCoord[2] = y2;
+ float value = 1.0f - (float)y2 / (float)PALETTE_DIM;
+
+ mHSV[2] = value;
+ updateAllFromHSV();
+ mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+ setOvalValDimmer();
+
+ invalidate();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ //NEW_METHOD_WORK_NEEDED_HERE
+ //To add a new method, replicate and extend the last entry in this list.
+ if (mTracking == TRACK_SWATCH_OLD && inSwatchOld) {
+ Color.colorToHSV(mOriginalColor, mHSV);
+ mSwatchNew.setColor(mOriginalColor);
+ initUI();
+ invalidate();
+ }
+ else if (mTracking == TRACK_SWATCH_NEW && inSwatchNew) {
+ mListener.colorChanged(mSwatchNew.getColor());
+ invalidate();
+ }
+
+ mTracking= TRACKED_NONE;
+ break;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/app/src/main/java/org/connectbot/util/VolumePreference.java b/app/src/main/java/org/connectbot/util/VolumePreference.java
new file mode 100644
index 0000000..2e7f61c
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/VolumePreference.java
@@ -0,0 +1,72 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+/**
+ * @author kenny
+ *
+ */
+public class VolumePreference extends DialogPreference implements OnSeekBarChangeListener {
+ /**
+ * @param context
+ * @param attrs
+ */
+ public VolumePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setupLayout(context, attrs);
+ }
+
+ public VolumePreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setupLayout(context, attrs);
+ }
+
+ private void setupLayout(Context context, AttributeSet attrs) {
+ setPersistent(true);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ SeekBar sb = new SeekBar(getContext());
+
+ sb.setMax(100);
+ sb.setProgress((int)(getPersistedFloat(
+ PreferenceConstants.DEFAULT_BELL_VOLUME) * 100));
+ sb.setPadding(10, 10, 10, 10);
+ sb.setOnSeekBarChangeListener(this);
+
+ return sb;
+ }
+
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ persistFloat(progress / 100f);
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) { }
+
+ public void onStopTrackingTouch(SeekBar seekBar) { }
+}
diff --git a/app/src/main/java/org/connectbot/util/XmlBuilder.java b/app/src/main/java/org/connectbot/util/XmlBuilder.java
new file mode 100644
index 0000000..4a6f62d
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/XmlBuilder.java
@@ -0,0 +1,71 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import com.trilead.ssh2.crypto.Base64;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class XmlBuilder {
+ private StringBuilder sb;
+
+ public XmlBuilder() {
+ sb = new StringBuilder();
+ }
+
+ public XmlBuilder append(String data) {
+ sb.append(data);
+
+ return this;
+ }
+
+ public XmlBuilder append(String field, Object data) {
+ if (data == null) {
+ sb.append(String.format("<%s/>", field));
+ } else if (data instanceof String) {
+ String input = (String) data;
+ boolean binary = false;
+
+ for (byte b : input.getBytes()) {
+ if (b < 0x20 || b > 0x7e) {
+ binary = true;
+ break;
+ }
+ }
+
+ sb.append(String.format("<%s>%s</%s>", field,
+ binary ? new String(Base64.encode(input.getBytes())) : input, field));
+ } else if (data instanceof Integer) {
+ sb.append(String.format("<%s>%d</%s>", field, (Integer) data, field));
+ } else if (data instanceof Long) {
+ sb.append(String.format("<%s>%d</%s>", field, (Long) data, field));
+ } else if (data instanceof byte[]) {
+ sb.append(String.format("<%s>%s</%s>", field, new String(Base64.encode((byte[]) data)), field));
+ } else if (data instanceof Boolean) {
+ sb.append(String.format("<%s>%s</%s>", field, (Boolean) data, field));
+ }
+
+ return this;
+ }
+
+ public String toString() {
+ return sb.toString();
+ }
+}