aboutsummaryrefslogtreecommitdiffstats
path: root/org_apg/src
diff options
context:
space:
mode:
authorDominik <dominik@dominikschuermann.de>2012-03-10 20:22:29 +0100
committerDominik <dominik@dominikschuermann.de>2012-03-10 20:22:29 +0100
commit630780cae4683e8ba403b28da84f963221d80ee3 (patch)
tree74ee9eac3a4c5640451e88b29a1b3350ca538def /org_apg/src
parent3fd5a55516eace832d2d2300ebf7a2579ab9c6dc (diff)
downloadopen-keychain-630780cae4683e8ba403b28da84f963221d80ee3.tar.gz
open-keychain-630780cae4683e8ba403b28da84f963221d80ee3.tar.bz2
open-keychain-630780cae4683e8ba403b28da84f963221d80ee3.zip
new directory structure
Diffstat (limited to 'org_apg/src')
-rw-r--r--org_apg/src/org/apg/Apg.java2283
-rw-r--r--org_apg/src/org/apg/ApgService.java643
-rw-r--r--org_apg/src/org/apg/AskForSecretKeyPassPhrase.java115
-rw-r--r--org_apg/src/org/apg/CachedPassPhrase.java48
-rw-r--r--org_apg/src/org/apg/Constants.java54
-rw-r--r--org_apg/src/org/apg/DataDestination.java81
-rw-r--r--org_apg/src/org/apg/DataSource.java104
-rw-r--r--org_apg/src/org/apg/FileDialog.java133
-rw-r--r--org_apg/src/org/apg/HkpKeyServer.java242
-rw-r--r--org_apg/src/org/apg/IApgService.aidl125
-rw-r--r--org_apg/src/org/apg/Id.java185
-rw-r--r--org_apg/src/org/apg/InputData.java25
-rw-r--r--org_apg/src/org/apg/KeyServer.java46
-rw-r--r--org_apg/src/org/apg/PausableThread.java35
-rw-r--r--org_apg/src/org/apg/PositionAwareInputStream.java67
-rw-r--r--org_apg/src/org/apg/Preferences.java170
-rw-r--r--org_apg/src/org/apg/Primes.java185
-rw-r--r--org_apg/src/org/apg/ProgressDialogUpdater.java25
-rw-r--r--org_apg/src/org/apg/Service.java78
-rw-r--r--org_apg/src/org/apg/provider/Accounts.java27
-rw-r--r--org_apg/src/org/apg/provider/ApgServiceBlobDatabase.java54
-rw-r--r--org_apg/src/org/apg/provider/ApgServiceBlobProvider.java138
-rw-r--r--org_apg/src/org/apg/provider/DataProvider.java381
-rw-r--r--org_apg/src/org/apg/provider/Database.java605
-rw-r--r--org_apg/src/org/apg/provider/KeyRings.java33
-rw-r--r--org_apg/src/org/apg/provider/Keys.java51
-rw-r--r--org_apg/src/org/apg/provider/UserIds.java31
-rw-r--r--org_apg/src/org/apg/ui/AboutActivity.java51
-rw-r--r--org_apg/src/org/apg/ui/BaseActivity.java436
-rw-r--r--org_apg/src/org/apg/ui/DecryptActivity.java807
-rw-r--r--org_apg/src/org/apg/ui/EditKeyActivity.java292
-rw-r--r--org_apg/src/org/apg/ui/EncryptActivity.java998
-rw-r--r--org_apg/src/org/apg/ui/GeneralActivity.java177
-rw-r--r--org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java138
-rw-r--r--org_apg/src/org/apg/ui/KeyListActivity.java768
-rw-r--r--org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java125
-rw-r--r--org_apg/src/org/apg/ui/KeyServerQueryActivity.java297
-rw-r--r--org_apg/src/org/apg/ui/MailListActivity.java222
-rw-r--r--org_apg/src/org/apg/ui/MainActivity.java419
-rw-r--r--org_apg/src/org/apg/ui/PreferencesActivity.java274
-rw-r--r--org_apg/src/org/apg/ui/PublicKeyListActivity.java191
-rw-r--r--org_apg/src/org/apg/ui/SecretKeyListActivity.java203
-rw-r--r--org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java172
-rw-r--r--org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java226
-rw-r--r--org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java115
-rw-r--r--org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java176
-rw-r--r--org_apg/src/org/apg/ui/SendKeyActivity.java100
-rw-r--r--org_apg/src/org/apg/ui/SignKeyActivity.java294
-rw-r--r--org_apg/src/org/apg/ui/widget/Editor.java25
-rw-r--r--org_apg/src/org/apg/ui/widget/IntegerListPreference.java95
-rw-r--r--org_apg/src/org/apg/ui/widget/KeyEditor.java233
-rw-r--r--org_apg/src/org/apg/ui/widget/KeyServerEditor.java78
-rw-r--r--org_apg/src/org/apg/ui/widget/SectionView.java335
-rw-r--r--org_apg/src/org/apg/ui/widget/UserIdEditor.java192
-rw-r--r--org_apg/src/org/apg/util/ApgCon.java836
-rw-r--r--org_apg/src/org/apg/util/ApgConInterface.java7
-rw-r--r--org_apg/src/org/apg/util/Choice.java45
-rw-r--r--org_apg/src/org/apg/util/Compatibility.java79
-rw-r--r--org_apg/src/org/apg/util/IterableIterator.java31
59 files changed, 14401 insertions, 0 deletions
diff --git a/org_apg/src/org/apg/Apg.java b/org_apg/src/org/apg/Apg.java
new file mode 100644
index 000000000..2bfdb31e6
--- /dev/null
+++ b/org_apg/src/org/apg/Apg.java
@@ -0,0 +1,2283 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import org.apg.KeyServer.AddKeyException;
+import org.apg.provider.DataProvider;
+import org.apg.provider.Database;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.ui.BaseActivity;
+import org.apg.ui.widget.KeyEditor;
+import org.apg.ui.widget.SectionView;
+import org.apg.ui.widget.UserIdEditor;
+import org.apg.util.IterableIterator;
+import org.spongycastle.bcpg.ArmoredInputStream;
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.spongycastle.bcpg.BCPGOutputStream;
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.jce.spec.ElGamalParameterSpec;
+import org.spongycastle.openpgp.PGPCompressedData;
+import org.spongycastle.openpgp.PGPCompressedDataGenerator;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.spongycastle.openpgp.PGPEncryptedDataGenerator;
+import org.spongycastle.openpgp.PGPEncryptedDataList;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPKeyPair;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPKeyRingGenerator;
+import org.spongycastle.openpgp.PGPLiteralData;
+import org.spongycastle.openpgp.PGPLiteralDataGenerator;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPOnePassSignature;
+import org.spongycastle.openpgp.PGPOnePassSignatureList;
+import org.spongycastle.openpgp.PGPPBEEncryptedData;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureList;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.spongycastle.openpgp.PGPV3SignatureGenerator;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Message;
+import android.view.ViewGroup;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.SignatureException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+import java.util.regex.Pattern;
+
+public class Apg {
+ private static final String mApgPackageName = "org.thialfihar.android.apg";
+
+ public static class Intent {
+ public static final String DECRYPT = "org.thialfihar.android.apg.intent.DECRYPT";
+ public static final String ENCRYPT = "org.thialfihar.android.apg.intent.ENCRYPT";
+ public static final String DECRYPT_FILE = "org.thialfihar.android.apg.intent.DECRYPT_FILE";
+ public static final String ENCRYPT_FILE = "org.thialfihar.android.apg.intent.ENCRYPT_FILE";
+ public static final String DECRYPT_AND_RETURN = "org.thialfihar.android.apg.intent.DECRYPT_AND_RETURN";
+ public static final String ENCRYPT_AND_RETURN = "org.thialfihar.android.apg.intent.ENCRYPT_AND_RETURN";
+ public static final String SELECT_PUBLIC_KEYS = "org.thialfihar.android.apg.intent.SELECT_PUBLIC_KEYS";
+ public static final String SELECT_SECRET_KEY = "org.thialfihar.android.apg.intent.SELECT_SECRET_KEY";
+ public static final String IMPORT = "org.thialfihar.android.apg.intent.IMPORT";
+ public static final String LOOK_UP_KEY_ID = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID";
+ public static final String LOOK_UP_KEY_ID_AND_RETURN = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID_AND_RETURN";
+ public static final String GENERATE_SIGNATURE = "org.thialfihar.android.apg.intent.GENERATE_SIGNATURE";
+ public static final String EXPORT_KEY_TO_SERVER = "org.thialfihar.android.apg.intent.EXPORT_KEY_TO_SERVER";
+ public static final String IMPORT_FROM_QR_CODE = "org.thialfihar.android.apg.intent.IMPORT_FROM_QR_CODE";
+ }
+
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_DATA = "data";
+ public static final String EXTRA_ERROR = "error";
+ public static final String EXTRA_DECRYPTED_MESSAGE = "decryptedMessage";
+ public static final String EXTRA_DECRYPTED_DATA = "decryptedData";
+ public static final String EXTRA_ENCRYPTED_MESSAGE = "encryptedMessage";
+ public static final String EXTRA_ENCRYPTED_DATA = "encryptedData";
+ public static final String EXTRA_RESULT_URI = "resultUri";
+ public static final String EXTRA_SIGNATURE = "signature";
+ public static final String EXTRA_SIGNATURE_KEY_ID = "signatureKeyId";
+ public static final String EXTRA_SIGNATURE_USER_ID = "signatureUserId";
+ public static final String EXTRA_SIGNATURE_SUCCESS = "signatureSuccess";
+ public static final String EXTRA_SIGNATURE_UNKNOWN = "signatureUnknown";
+ public static final String EXTRA_SIGNATURE_DATA = "signatureData";
+ public static final String EXTRA_SIGNATURE_TEXT = "signatureText";
+ public static final String EXTRA_USER_ID = "userId";
+ public static final String EXTRA_USER_IDS = "userIds";
+ public static final String EXTRA_KEY_ID = "keyId";
+ public static final String EXTRA_REPLY_TO = "replyTo";
+ public static final String EXTRA_SEND_TO = "sendTo";
+ public static final String EXTRA_SUBJECT = "subject";
+ public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryptionKeyIds";
+ public static final String EXTRA_SELECTION = "selection";
+ public static final String EXTRA_ASCII_ARMOUR = "asciiArmour";
+ public static final String EXTRA_BINARY = "binary";
+ public static final String EXTRA_KEY_SERVERS = "keyServers";
+ public static final String EXTRA_EXPECTED_FINGERPRINT = "org.thialfihar.android.apg.EXPECTED_FINGERPRINT";
+
+ public static final String AUTHORITY = DataProvider.AUTHORITY;
+
+ public static final Uri CONTENT_URI_SECRET_KEY_RINGS = Uri.parse("content://" + AUTHORITY
+ + "/key_rings/secret/");
+ public static final Uri CONTENT_URI_SECRET_KEY_RING_BY_KEY_ID = Uri.parse("content://"
+ + AUTHORITY + "/key_rings/secret/key_id/");
+ public static final Uri CONTENT_URI_SECRET_KEY_RING_BY_EMAILS = Uri.parse("content://"
+ + AUTHORITY + "/key_rings/secret/emails/");
+
+ public static final Uri CONTENT_URI_PUBLIC_KEY_RINGS = Uri.parse("content://" + AUTHORITY
+ + "/key_rings/public/");
+ public static final Uri CONTENT_URI_PUBLIC_KEY_RING_BY_KEY_ID = Uri.parse("content://"
+ + AUTHORITY + "/key_rings/public/key_id/");
+ public static final Uri CONTENT_URI_PUBLIC_KEY_RING_BY_EMAILS = Uri.parse("content://"
+ + AUTHORITY + "/key_rings/public/emails/");
+
+ private static String VERSION = null;
+
+ private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[] {
+ SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
+ SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5,
+ SymmetricKeyAlgorithmTags.TRIPLE_DES };
+ private static final int[] PREFERRED_HASH_ALGORITHMS = new int[] { HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160 };
+ private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[] {
+ CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
+ CompressionAlgorithmTags.ZIP };
+
+ public static Pattern PGP_MESSAGE = Pattern.compile(
+ ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
+
+ public static Pattern PGP_SIGNED_MESSAGE = Pattern
+ .compile(
+ ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
+ Pattern.DOTALL);
+
+ public static Pattern PGP_PUBLIC_KEY = Pattern.compile(
+ ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
+ Pattern.DOTALL);
+
+ private static HashMap<Long, CachedPassPhrase> mPassPhraseCache = new HashMap<Long, CachedPassPhrase>();
+ private static String mEditPassPhrase = null;
+
+ private static Database mDatabase = null;
+
+ public static class GeneralException extends Exception {
+ static final long serialVersionUID = 0xf812773342L;
+
+ public GeneralException(String message) {
+ super(message);
+ }
+ }
+
+ public static class NoAsymmetricEncryptionException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public NoAsymmetricEncryptionException() {
+ super();
+ }
+ }
+
+ public static void initialize(Context context) {
+ if (mDatabase == null) {
+ mDatabase = new Database(context);
+ }
+ }
+
+ public static Database getDatabase() {
+ return mDatabase;
+ }
+
+ public static void setEditPassPhrase(String passPhrase) {
+ mEditPassPhrase = passPhrase;
+ }
+
+ public static String getEditPassPhrase() {
+ return mEditPassPhrase;
+ }
+
+ public static void setCachedPassPhrase(long keyId, String passPhrase) {
+ mPassPhraseCache.put(keyId, new CachedPassPhrase(new Date().getTime(), passPhrase));
+ }
+
+ public static String getCachedPassPhrase(long keyId) {
+ long realId = keyId;
+ if (realId != Id.key.symmetric) {
+ PGPSecretKeyRing keyRing = getSecretKeyRing(keyId);
+ if (keyRing == null) {
+ return null;
+ }
+ PGPSecretKey masterKey = getMasterKey(keyRing);
+ if (masterKey == null) {
+ return null;
+ }
+ realId = masterKey.getKeyID();
+ }
+ CachedPassPhrase cpp = mPassPhraseCache.get(realId);
+ if (cpp == null) {
+ return null;
+ }
+ // set it again to reset the cache life cycle
+ setCachedPassPhrase(realId, cpp.passPhrase);
+ return cpp.passPhrase;
+ }
+
+ public static int cleanUpCache(int ttl, int initialDelay) {
+ int delay = initialDelay;
+ long realTtl = ttl * 1000;
+ long now = new Date().getTime();
+ Vector<Long> oldKeys = new Vector<Long>();
+ for (Map.Entry<Long, CachedPassPhrase> pair : mPassPhraseCache.entrySet()) {
+ long lived = now - pair.getValue().timestamp;
+ if (lived >= realTtl) {
+ oldKeys.add(pair.getKey());
+ } else {
+ // see, whether the remaining time for this cache entry improves our
+ // check delay
+ long nextCheck = realTtl - lived + 1000;
+ if (nextCheck < delay) {
+ delay = (int) nextCheck;
+ }
+ }
+ }
+
+ for (long keyId : oldKeys) {
+ mPassPhraseCache.remove(keyId);
+ }
+
+ return delay;
+ }
+
+ public static PGPSecretKey createKey(Context context, int algorithmChoice, int keySize,
+ String passPhrase, PGPSecretKey masterKey) throws NoSuchAlgorithmException,
+ PGPException, NoSuchProviderException, GeneralException,
+ InvalidAlgorithmParameterException {
+
+ if (keySize < 512) {
+ throw new GeneralException(context.getString(R.string.error_keySizeMinimum512bit));
+ }
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ if (passPhrase == null) {
+ passPhrase = "";
+ }
+
+ int algorithm = 0;
+ KeyPairGenerator keyGen = null;
+
+ switch (algorithmChoice) {
+ case Id.choice.algorithm.dsa: {
+ keyGen = KeyPairGenerator.getInstance("DSA", new BouncyCastleProvider());
+ keyGen.initialize(keySize, new SecureRandom());
+ algorithm = PGPPublicKey.DSA;
+ break;
+ }
+
+ case Id.choice.algorithm.elgamal: {
+ if (masterKey == null) {
+ throw new GeneralException(
+ context.getString(R.string.error_masterKeyMustNotBeElGamal));
+ }
+ keyGen = KeyPairGenerator.getInstance("ELGAMAL", new BouncyCastleProvider());
+ BigInteger p = Primes.getBestPrime(keySize);
+ BigInteger g = new BigInteger("2");
+
+ ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);
+
+ keyGen.initialize(elParams);
+ algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
+ break;
+ }
+
+ case Id.choice.algorithm.rsa: {
+ keyGen = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
+ keyGen.initialize(keySize, new SecureRandom());
+
+ algorithm = PGPPublicKey.RSA_GENERAL;
+ break;
+ }
+
+ default: {
+ throw new GeneralException(context.getString(R.string.error_unknownAlgorithmChoice));
+ }
+ }
+
+ PGPKeyPair keyPair = new PGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
+
+ PGPSecretKey secretKey = null;
+ if (masterKey == null) {
+ // enough for now, as we assemble the key again later anyway
+ secretKey = new PGPSecretKey(PGPSignature.DEFAULT_CERTIFICATION, keyPair, "",
+ PGPEncryptedData.CAST5, passPhrase.toCharArray(), null, null,
+ new SecureRandom(), new BouncyCastleProvider().getName());
+
+ } else {
+ PGPPublicKey tmpKey = masterKey.getPublicKey();
+ PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
+ tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
+ PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(passPhrase.toCharArray(),
+ new BouncyCastleProvider());
+
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+ PGPKeyRingGenerator ringGen = new PGPKeyRingGenerator(
+ PGPSignature.POSITIVE_CERTIFICATION, masterKeyPair, "", PGPEncryptedData.CAST5,
+ passPhrase.toCharArray(), null, null, new SecureRandom(),
+ new BouncyCastleProvider().getName());
+ ringGen.addSubKey(keyPair);
+ PGPSecretKeyRing secKeyRing = ringGen.generateSecretKeyRing();
+ Iterator<PGPSecretKey> it = secKeyRing.getSecretKeys();
+ // first one is the master key
+ it.next();
+ secretKey = it.next();
+ }
+
+ return secretKey;
+ }
+
+ private static long getNumDaysBetween(GregorianCalendar first, GregorianCalendar second) {
+ GregorianCalendar tmp = new GregorianCalendar();
+ tmp.setTime(first.getTime());
+ long numDays = (second.getTimeInMillis() - first.getTimeInMillis()) / 1000 / 86400;
+ tmp.add(Calendar.DAY_OF_MONTH, (int) numDays);
+ while (tmp.before(second)) {
+ tmp.add(Calendar.DAY_OF_MONTH, 1);
+ ++numDays;
+ }
+ return numDays;
+ }
+
+ public static void buildSecretKey(Activity context, SectionView userIdsView,
+ SectionView keysView, String oldPassPhrase, String newPassPhrase,
+ ProgressDialogUpdater progress) throws Apg.GeneralException, NoSuchProviderException,
+ PGPException, NoSuchAlgorithmException, SignatureException, IOException,
+ Database.GeneralException {
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_buildingKey, 0, 100);
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ if (oldPassPhrase == null || oldPassPhrase.equals("")) {
+ oldPassPhrase = "";
+ }
+
+ if (newPassPhrase == null || newPassPhrase.equals("")) {
+ newPassPhrase = "";
+ }
+
+ Vector<String> userIds = new Vector<String>();
+ Vector<PGPSecretKey> keys = new Vector<PGPSecretKey>();
+
+ ViewGroup userIdEditors = userIdsView.getEditors();
+ ViewGroup keyEditors = keysView.getEditors();
+
+ boolean gotMainUserId = false;
+ for (int i = 0; i < userIdEditors.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i);
+ String userId = null;
+ try {
+ userId = editor.getValue();
+ } catch (UserIdEditor.NoNameException e) {
+ throw new Apg.GeneralException(context.getString(R.string.error_userIdNeedsAName));
+ } catch (UserIdEditor.NoEmailException e) {
+ throw new Apg.GeneralException(
+ context.getString(R.string.error_userIdNeedsAnEmailAddress));
+ } catch (UserIdEditor.InvalidEmailException e) {
+ throw new Apg.GeneralException("" + e);
+ }
+
+ if (userId.equals("")) {
+ continue;
+ }
+
+ if (editor.isMainUserId()) {
+ userIds.insertElementAt(userId, 0);
+ gotMainUserId = true;
+ } else {
+ userIds.add(userId);
+ }
+ }
+
+ if (userIds.size() == 0) {
+ throw new Apg.GeneralException(context.getString(R.string.error_keyNeedsAUserId));
+ }
+
+ if (!gotMainUserId) {
+ throw new Apg.GeneralException(
+ context.getString(R.string.error_mainUserIdMustNotBeEmpty));
+ }
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new Apg.GeneralException(context.getString(R.string.error_keyNeedsMasterKey));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ keys.add(editor.getValue());
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingMasterKey, 10, 100);
+ KeyEditor keyEditor = (KeyEditor) keyEditors.getChildAt(0);
+ int usageId = keyEditor.getUsage();
+ boolean canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
+ boolean canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
+
+ String mainUserId = userIds.get(0);
+
+ PGPSecretKey masterKey = keys.get(0);
+ PGPPublicKey tmpKey = masterKey.getPublicKey();
+ PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
+ tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
+ PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(oldPassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_certifyingMasterKey, 20, 100);
+ for (int i = 0; i < userIds.size(); ++i) {
+ String userId = userIds.get(i);
+
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(masterPublicKey.getAlgorithm(),
+ HashAlgorithmTags.SHA1, new BouncyCastleProvider());
+
+ sGen.initSign(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
+
+ PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
+
+ masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
+ }
+
+ // TODO: cross-certify the master key with every sub key
+
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+
+ PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
+ if (canEncrypt) {
+ keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ }
+ hashedPacketsGen.setKeyFlags(true, keyFlags);
+
+ hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
+ hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
+ hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
+
+ // TODO: this doesn't work quite right yet
+ if (keyEditor.getExpiryDate() != null) {
+ GregorianCalendar creationDate = new GregorianCalendar();
+ creationDate.setTime(getCreationDate(masterKey));
+ GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ long numDays = getNumDaysBetween(creationDate, expiryDate);
+ if (numDays <= 0) {
+ throw new GeneralException(
+ context.getString(R.string.error_expiryMustComeAfterCreation));
+ }
+ hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_buildingMasterKeyRing, 30, 100);
+ PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
+ masterKeyPair, mainUserId, PGPEncryptedData.CAST5, newPassPhrase.toCharArray(),
+ hashedPacketsGen.generate(), unhashedPacketsGen.generate(), new SecureRandom(),
+ new BouncyCastleProvider().getName());
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_addingSubKeys, 40, 100);
+ for (int i = 1; i < keys.size(); ++i) {
+ if (progress != null)
+ progress.setProgress(40 + 50 * (i - 1) / (keys.size() - 1), 100);
+ PGPSecretKey subKey = keys.get(i);
+ keyEditor = (KeyEditor) keyEditors.getChildAt(i);
+ PGPPublicKey subPublicKey = subKey.getPublicKey();
+ PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(oldPassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey.getAlgorithm(),
+ subPublicKey.getKey(new BouncyCastleProvider()), subPrivateKey.getKey(),
+ subPublicKey.getCreationTime());
+
+ hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ keyFlags = 0;
+ usageId = keyEditor.getUsage();
+ canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
+ canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
+ if (canSign) {
+ keyFlags |= KeyFlags.SIGN_DATA;
+ }
+ if (canEncrypt) {
+ keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ }
+ hashedPacketsGen.setKeyFlags(true, keyFlags);
+
+ // TODO: this doesn't work quite right yet
+ if (keyEditor.getExpiryDate() != null) {
+ GregorianCalendar creationDate = new GregorianCalendar();
+ creationDate.setTime(getCreationDate(masterKey));
+ GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ long numDays = getNumDaysBetween(creationDate, expiryDate);
+ if (numDays <= 0) {
+ throw new GeneralException(
+ context.getString(R.string.error_expiryMustComeAfterCreation));
+ }
+ hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ }
+
+ keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
+ }
+
+ PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
+ PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_savingKeyRing, 90, 100);
+ mDatabase.saveKeyRing(secretKeyRing);
+ mDatabase.saveKeyRing(publicKeyRing);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ }
+
+ public static PGPKeyRing decodeKeyRing(InputStream is) throws IOException {
+ InputStream in = PGPUtil.getDecoderStream(is);
+ PGPObjectFactory objectFactory = new PGPObjectFactory(in);
+ Object obj = objectFactory.nextObject();
+
+ if (obj instanceof PGPKeyRing) {
+ return (PGPKeyRing) obj;
+ }
+
+ return null;
+ }
+
+ public static int storeKeyRingInCache(PGPKeyRing keyring) {
+ int status = Integer.MIN_VALUE; // out of bounds value (Id.retrun_value.*)
+ try {
+ if (keyring instanceof PGPSecretKeyRing) {
+ PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
+ boolean save = true;
+ try {
+ PGPPrivateKey testKey = secretKeyRing.getSecretKey().extractPrivateKey(
+ new char[] {}, new BouncyCastleProvider());
+ if (testKey == null) {
+ // this is bad, something is very wrong... likely a --export-secret-subkeys
+ // export
+ save = false;
+ status = Id.return_value.bad;
+ }
+ } catch (PGPException e) {
+ // all good if this fails, we likely didn't use the right password
+ }
+
+ if (save) {
+ status = mDatabase.saveKeyRing(secretKeyRing);
+ }
+ } else if (keyring instanceof PGPPublicKeyRing) {
+ PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+ status = mDatabase.saveKeyRing(publicKeyRing);
+ }
+ } catch (IOException e) {
+ status = Id.return_value.error;
+ } catch (Database.GeneralException e) {
+ status = Id.return_value.error;
+ }
+
+ return status;
+ }
+
+ public static boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ArmoredOutputStream aos = new ArmoredOutputStream(bos);
+ try {
+ aos.write(keyring.getEncoded());
+ aos.close();
+
+ String armouredKey = bos.toString("UTF-8");
+ server.add(armouredKey);
+
+ return true;
+ } catch (IOException e) {
+ return false;
+ } catch (AddKeyException e) {
+ // TODO: tell the user?
+ return false;
+ } finally {
+ try {
+ bos.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public static Bundle importKeyRings(Activity context, int type, InputData data,
+ ProgressDialogUpdater progress) throws GeneralException, FileNotFoundException,
+ PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ if (type == Id.type.secret_key) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_importingSecretKeys, 0, 100);
+ } else {
+ if (progress != null)
+ progress.setProgress(R.string.progress_importingPublicKeys, 0, 100);
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException(context.getString(R.string.error_externalStorageNotReady));
+ }
+
+ PositionAwareInputStream progressIn = new PositionAwareInputStream(data.getInputStream());
+ // need to have access to the bufferedInput, so we can reuse it for the possible
+ // PGPObject chunks after the first one, e.g. files with several consecutive ASCII
+ // armour blocks
+ BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
+ int newKeys = 0;
+ int oldKeys = 0;
+ int badKeys = 0;
+ try {
+ PGPKeyRing keyring = decodeKeyRing(bufferedInput);
+ while (keyring != null) {
+ int status = Integer.MIN_VALUE; // out of bounds value
+
+ // if this key is what we expect it to be, save it
+ if ((type == Id.type.secret_key && keyring instanceof PGPSecretKeyRing)
+ || (type == Id.type.public_key && keyring instanceof PGPPublicKeyRing)) {
+ status = storeKeyRingInCache(keyring);
+ }
+
+ if (status == Id.return_value.error) {
+ throw new GeneralException(context.getString(R.string.error_savingKeys));
+ }
+
+ // update the counts to display to the user at the end
+ if (status == Id.return_value.updated) {
+ ++oldKeys;
+ } else if (status == Id.return_value.ok) {
+ ++newKeys;
+ } else if (status == Id.return_value.bad) {
+ ++badKeys;
+ }
+
+ if (progress != null) {
+ progress.setProgress((int) (100 * progressIn.position() / data.getSize()), 100);
+ }
+ // TODO: needed?
+ // obj = objectFactory.nextObject();
+
+ keyring = decodeKeyRing(bufferedInput);
+ }
+ } catch (EOFException e) {
+ // nothing to do, we are done
+ }
+
+ returnData.putInt("added", newKeys);
+ returnData.putInt("updated", oldKeys);
+ returnData.putInt("bad", badKeys);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+
+ return returnData;
+ }
+
+ public static Bundle exportKeyRings(Activity context, Vector<Integer> keyRingIds,
+ OutputStream outStream, ProgressDialogUpdater progress) throws GeneralException,
+ FileNotFoundException, PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ if (keyRingIds.size() == 1) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_exportingKey, 0, 100);
+ } else {
+ if (progress != null)
+ progress.setProgress(R.string.progress_exportingKeys, 0, 100);
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException(context.getString(R.string.error_externalStorageNotReady));
+ }
+ ArmoredOutputStream out = new ArmoredOutputStream(outStream);
+
+ int numKeys = 0;
+ for (int i = 0; i < keyRingIds.size(); ++i) {
+ if (progress != null)
+ progress.setProgress(i * 100 / keyRingIds.size(), 100);
+ Object obj = mDatabase.getKeyRing(keyRingIds.get(i));
+ PGPPublicKeyRing publicKeyRing;
+ PGPSecretKeyRing secretKeyRing;
+
+ if (obj instanceof PGPSecretKeyRing) {
+ secretKeyRing = (PGPSecretKeyRing) obj;
+ secretKeyRing.encode(out);
+ } else if (obj instanceof PGPPublicKeyRing) {
+ publicKeyRing = (PGPPublicKeyRing) obj;
+ publicKeyRing.encode(out);
+ } else {
+ continue;
+ }
+ ++numKeys;
+ }
+ out.close();
+ returnData.putInt("exported", numKeys);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+
+ return returnData;
+ }
+
+ public static Date getCreationDate(PGPPublicKey key) {
+ return key.getCreationTime();
+ }
+
+ public static Date getCreationDate(PGPSecretKey key) {
+ return key.getPublicKey().getCreationTime();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) {
+ if (keyRing == null) {
+ return null;
+ }
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) {
+ if (keyRing == null) {
+ return null;
+ }
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>();
+
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (isEncryptionKey(key)) {
+ encryptKeys.add(key);
+ }
+ }
+
+ return encryptKeys;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
+
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (isSigningKey(key)) {
+ signingKeys.add(key);
+ }
+ }
+
+ return signingKeys;
+ }
+
+ public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>();
+ Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing);
+ PGPPublicKey masterKey = null;
+ for (int i = 0; i < encryptKeys.size(); ++i) {
+ PGPPublicKey key = encryptKeys.get(i);
+ if (!isExpired(key)) {
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static boolean isExpired(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ Date expiryDate = getExpiryDate(key);
+ Date now = new Date();
+ if (now.compareTo(creationDate) >= 0
+ && (expiryDate == null || now.compareTo(expiryDate) <= 0)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isExpired(PGPSecretKey key) {
+ return isExpired(key.getPublicKey());
+ }
+
+ public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
+ Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing);
+ PGPSecretKey masterKey = null;
+ for (int i = 0; i < signingKeys.size(); ++i) {
+ PGPSecretKey key = signingKeys.get(i);
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static Date getExpiryDate(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ if (key.getValidDays() == 0) {
+ // no expiry
+ return null;
+ }
+ Calendar calendar = GregorianCalendar.getInstance();
+ calendar.setTime(creationDate);
+ calendar.add(Calendar.DATE, key.getValidDays());
+ Date expiryDate = calendar.getTime();
+
+ return expiryDate;
+ }
+
+ public static Date getExpiryDate(PGPSecretKey key) {
+ return getExpiryDate(key.getPublicKey());
+ }
+
+ public static PGPPublicKey getEncryptPublicKey(long masterKeyId) {
+ PGPPublicKeyRing keyRing = getPublicKeyRing(masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ return null;
+ }
+ return encryptKeys.get(0);
+ }
+
+ public static PGPSecretKey getSigningKey(long masterKeyId) {
+ PGPSecretKeyRing keyRing = getSecretKeyRing(masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
+ if (signingKeys.size() == 0) {
+ return null;
+ }
+ return signingKeys.get(0);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPPublicKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPSecretKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPPublicKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null) {
+ userId = context.getResources().getString(R.string.unknownUserId);
+ }
+ return userId;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null) {
+ userId = context.getResources().getString(R.string.unknownUserId);
+ }
+ return userId;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isEncryptionKey(PGPPublicKey key) {
+ if (!key.isEncryptionKey()) {
+ return false;
+ }
+
+ if (key.getVersion() <= 3) {
+ // this must be true now
+ return key.isEncryptionKey();
+ }
+
+ // special cases
+ if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
+ return true;
+ }
+
+ if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null
+ && (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null
+ && (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isEncryptionKey(PGPSecretKey key) {
+ return isEncryptionKey(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isSigningKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ // special case
+ if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isSigningKey(PGPSecretKey key) {
+ return isSigningKey(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(PGPPublicKey key) {
+ return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength());
+ }
+
+ public static String getAlgorithmInfo(PGPSecretKey key) {
+ return getAlgorithmInfo(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(int algorithm, int keySize) {
+ String algorithmStr = null;
+
+ switch (algorithm) {
+ case PGPPublicKey.RSA_ENCRYPT:
+ case PGPPublicKey.RSA_GENERAL:
+ case PGPPublicKey.RSA_SIGN: {
+ algorithmStr = "RSA";
+ break;
+ }
+
+ case PGPPublicKey.DSA: {
+ algorithmStr = "DSA";
+ break;
+ }
+
+ case PGPPublicKey.ELGAMAL_ENCRYPT:
+ case PGPPublicKey.ELGAMAL_GENERAL: {
+ algorithmStr = "ElGamal";
+ break;
+ }
+
+ default: {
+ algorithmStr = "???";
+ break;
+ }
+ }
+ return algorithmStr + ", " + keySize + "bit";
+ }
+
+ public static String convertToHex(byte[] fp) {
+ String fingerPrint = "";
+ for (int i = 0; i < fp.length; ++i) {
+ if (i != 0 && i % 10 == 0) {
+ fingerPrint += " ";
+ } else if (i != 0 && i % 2 == 0) {
+ fingerPrint += " ";
+ }
+ String chunk = Integer.toHexString((fp[i] + 256) % 256).toUpperCase();
+ while (chunk.length() < 2) {
+ chunk = "0" + chunk;
+ }
+ fingerPrint += chunk;
+ }
+
+ return fingerPrint;
+
+ }
+
+ public static String getFingerPrint(long keyId) {
+ PGPPublicKey key = Apg.getPublicKey(keyId);
+ if (key == null) {
+ PGPSecretKey secretKey = Apg.getSecretKey(keyId);
+ if (secretKey == null) {
+ return "";
+ }
+ key = secretKey.getPublicKey();
+ }
+
+ return convertToHex(key.getFingerprint());
+ }
+
+ public static String getSmallFingerPrint(long keyId) {
+ String fingerPrint = Long.toHexString(keyId & 0xffffffffL).toUpperCase();
+ while (fingerPrint.length() < 8) {
+ fingerPrint = "0" + fingerPrint;
+ }
+ return fingerPrint;
+ }
+
+ public static String keyToHex(long keyId) {
+ return getSmallFingerPrint(keyId >> 32) + getSmallFingerPrint(keyId);
+ }
+
+ public static long keyFromHex(String data) {
+ int len = data.length();
+ String s2 = data.substring(len - 8);
+ String s1 = data.substring(0, len - 8);
+ return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16);
+ }
+
+ public static void deleteKey(int keyRingId) {
+ mDatabase.deleteKeyRing(keyRingId);
+ }
+
+ public static PGPKeyRing getKeyRing(int keyRingId) {
+ return (PGPKeyRing) mDatabase.getKeyRing(keyRingId);
+ }
+
+ public static PGPSecretKeyRing getSecretKeyRing(long keyId) {
+ byte[] data = mDatabase.getKeyRingDataFromKeyId(Id.database.type_secret, keyId);
+ if (data == null) {
+ return null;
+ }
+ try {
+ return new PGPSecretKeyRing(data);
+ } catch (IOException e) {
+ // no good way to handle this, return null
+ // TODO: some info?
+ } catch (PGPException e) {
+ // no good way to handle this, return null
+ // TODO: some info?
+ }
+ return null;
+ }
+
+ public static PGPPublicKeyRing getPublicKeyRing(long keyId) {
+ byte[] data = mDatabase.getKeyRingDataFromKeyId(Id.database.type_public, keyId);
+ if (data == null) {
+ return null;
+ }
+ try {
+ return new PGPPublicKeyRing(data);
+ } catch (IOException e) {
+ // no good way to handle this, return null
+ // TODO: some info?
+ }
+ return null;
+ }
+
+ public static PGPSecretKey getSecretKey(long keyId) {
+ PGPSecretKeyRing keyRing = getSecretKeyRing(keyId);
+ if (keyRing == null) {
+ return null;
+ }
+ return keyRing.getSecretKey(keyId);
+ }
+
+ public static PGPPublicKey getPublicKey(long keyId) {
+ PGPPublicKeyRing keyRing = getPublicKeyRing(keyId);
+ if (keyRing == null) {
+ return null;
+ }
+
+ return keyRing.getPublicKey(keyId);
+ }
+
+ public static Vector<Integer> getKeyRingIds(int type) {
+ SQLiteDatabase db = mDatabase.db();
+ Vector<Integer> keyIds = new Vector<Integer>();
+ Cursor c = db.query(KeyRings.TABLE_NAME, new String[] { KeyRings._ID }, KeyRings.TYPE
+ + " = ?", new String[] { "" + type }, null, null, null);
+ if (c != null && c.moveToFirst()) {
+ do {
+ keyIds.add(c.getInt(0));
+ } while (c.moveToNext());
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return keyIds;
+ }
+
+ public static String getMainUserId(long keyId, int type) {
+ SQLiteDatabase db = mDatabase.db();
+ Cursor c = db.query(Keys.TABLE_NAME + " INNER JOIN " + KeyRings.TABLE_NAME + " ON ("
+ + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + Keys.TABLE_NAME + "."
+ + Keys.KEY_RING_ID + ") " + " INNER JOIN " + Keys.TABLE_NAME + " AS masterKey ON ("
+ + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + "masterKey."
+ + Keys.KEY_RING_ID + " AND " + "masterKey." + Keys.IS_MASTER_KEY + " = '1') "
+ + " INNER JOIN " + UserIds.TABLE_NAME + " ON (" + UserIds.TABLE_NAME + "."
+ + UserIds.KEY_ID + " = " + "masterKey." + Keys._ID + " AND " + UserIds.TABLE_NAME
+ + "." + UserIds.RANK + " = '0')", new String[] { UserIds.USER_ID }, Keys.TABLE_NAME
+ + "." + Keys.KEY_ID + " = ? AND " + KeyRings.TABLE_NAME + "." + KeyRings.TYPE
+ + " = ?", new String[] { "" + keyId, "" + type, }, null, null, null);
+ String userId = "";
+ if (c != null && c.moveToFirst()) {
+ do {
+ userId = c.getString(0);
+ } while (c.moveToNext());
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return userId;
+ }
+
+ public static void encrypt(Context context, InputData data, OutputStream outStream,
+ boolean armored, long encryptionKeyIds[], long signatureKeyId,
+ String signaturePassPhrase, ProgressDialogUpdater progress, int symmetricAlgorithm,
+ int hashAlgorithm, int compression, boolean forceV3Signature, String passPhrase)
+ throws IOException, GeneralException, PGPException, NoSuchProviderException,
+ NoSuchAlgorithmException, SignatureException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ if (encryptionKeyIds == null) {
+ encryptionKeyIds = new long[0];
+ }
+
+ ArmoredOutputStream armorOut = null;
+ OutputStream out = null;
+ OutputStream encryptOut = null;
+ if (armored) {
+ armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+ out = armorOut;
+ } else {
+ out = outStream;
+ }
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (encryptionKeyIds.length == 0 && passPhrase == null) {
+ throw new GeneralException(
+ context.getString(R.string.error_noEncryptionKeysOrPassPhrase));
+ }
+
+ if (signatureKeyId != 0) {
+ signingKeyRing = getSecretKeyRing(signatureKeyId);
+ signingKey = getSigningKey(signatureKeyId);
+ if (signingKey == null) {
+ throw new GeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new GeneralException(context.getString(R.string.error_noSignaturePassPhrase));
+ }
+ if (progress != null)
+ progress.setProgress(R.string.progress_extractingSignatureKey, 0, 100);
+ signaturePrivateKey = signingKey.extractPrivateKey(signaturePassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ if (signaturePrivateKey == null) {
+ throw new GeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ }
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingStreams, 5, 100);
+
+ // encrypt and compress input file content
+ PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(symmetricAlgorithm, true,
+ new SecureRandom(), new BouncyCastleProvider());
+
+ if (encryptionKeyIds.length == 0) {
+ // symmetric encryption
+ cPk.addMethod(passPhrase.toCharArray());
+ }
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKey key = getEncryptPublicKey(encryptionKeyIds[i]);
+ if (key != null) {
+ cPk.addMethod(key);
+ }
+ }
+ encryptOut = cPk.open(out, new byte[1 << 16]);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ if (signatureKeyId != 0) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingSignature, 10, 100);
+ if (forceV3Signature) {
+ signatureV3Generator = new PGPV3SignatureGenerator(signingKey.getPublicKey()
+ .getAlgorithm(), hashAlgorithm, new BouncyCastleProvider());
+ signatureV3Generator.initSign(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(signingKey.getPublicKey()
+ .getAlgorithm(), hashAlgorithm, new BouncyCastleProvider());
+ signatureGenerator.initSign(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey);
+
+ String userId = getMainUserId(getMasterKey(signingKeyRing));
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+ }
+
+ PGPCompressedDataGenerator compressGen = null;
+ BCPGOutputStream bcpgOut = null;
+ if (compression == Id.choice.compression.none) {
+ bcpgOut = new BCPGOutputStream(encryptOut);
+ } else {
+ compressGen = new PGPCompressedDataGenerator(compression);
+ bcpgOut = new BCPGOutputStream(compressGen.open(encryptOut));
+ }
+ if (signatureKeyId != 0) {
+ if (forceV3Signature) {
+ signatureV3Generator.generateOnePassVersion(false).encode(bcpgOut);
+ } else {
+ signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);
+ }
+ }
+
+ PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
+ // file name not needed, so empty string
+ OutputStream pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "", new Date(),
+ new byte[1 << 16]);
+ if (progress != null)
+ progress.setProgress(R.string.progress_encrypting, 20, 100);
+
+ long done = 0;
+ int n = 0;
+ byte[] buffer = new byte[1 << 16];
+ InputStream in = data.getInputStream();
+ while ((n = in.read(buffer)) > 0) {
+ pOut.write(buffer, 0, n);
+ if (signatureKeyId != 0) {
+ if (forceV3Signature) {
+ signatureV3Generator.update(buffer, 0, n);
+ } else {
+ signatureGenerator.update(buffer, 0, n);
+ }
+ }
+ done += n;
+ if (data.getSize() != 0) {
+ if (progress != null)
+ progress.setProgress((int) (20 + (95 - 20) * done / data.getSize()), 100);
+ }
+ }
+
+ literalGen.close();
+
+ if (signatureKeyId != 0) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_generatingSignature, 95, 100);
+ if (forceV3Signature) {
+ signatureV3Generator.generate().encode(pOut);
+ } else {
+ signatureGenerator.generate().encode(pOut);
+ }
+ }
+ if (compressGen != null) {
+ compressGen.close();
+ }
+ encryptOut.close();
+ if (armored) {
+ armorOut.close();
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ }
+
+ public static void signText(Context context, InputData data, OutputStream outStream,
+ long signatureKeyId, String signaturePassPhrase, int hashAlgorithm,
+ boolean forceV3Signature, ProgressDialogUpdater progress) throws GeneralException,
+ PGPException, IOException, NoSuchAlgorithmException, SignatureException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (signatureKeyId == 0) {
+ throw new GeneralException(context.getString(R.string.error_noSignatureKey));
+ }
+
+ signingKeyRing = getSecretKeyRing(signatureKeyId);
+ signingKey = getSigningKey(signatureKeyId);
+ if (signingKey == null) {
+ throw new GeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new GeneralException(context.getString(R.string.error_noSignaturePassPhrase));
+ }
+ signaturePrivateKey = signingKey.extractPrivateKey(signaturePassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ if (signaturePrivateKey == null) {
+ throw new GeneralException(context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingStreams, 0, 100);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingSignature, 30, 100);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ if (forceV3Signature) {
+ signatureV3Generator = new PGPV3SignatureGenerator(signingKey.getPublicKey()
+ .getAlgorithm(), hashAlgorithm, new BouncyCastleProvider());
+ signatureV3Generator
+ .initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(
+ signingKey.getPublicKey().getAlgorithm(), hashAlgorithm,
+ new BouncyCastleProvider());
+ signatureGenerator.initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ String userId = getMainUserId(getMasterKey(signingKeyRing));
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_signing, 40, 100);
+
+ armorOut.beginClearText(hashAlgorithm);
+
+ InputStream inStream = data.getInputStream();
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
+
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ if (forceV3Signature) {
+ processLine(reader.readLine(), armorOut, signatureV3Generator);
+ } else {
+ processLine(reader.readLine(), armorOut, signatureGenerator);
+ }
+
+ while (true) {
+ final String line = reader.readLine();
+
+ if (line == null) {
+ armorOut.write(newline);
+ break;
+ }
+
+ armorOut.write(newline);
+ if (forceV3Signature) {
+ signatureV3Generator.update(newline);
+ processLine(line, armorOut, signatureV3Generator);
+ } else {
+ signatureGenerator.update(newline);
+ processLine(line, armorOut, signatureGenerator);
+ }
+ }
+
+ armorOut.endClearText();
+
+ BCPGOutputStream bOut = new BCPGOutputStream(armorOut);
+ if (forceV3Signature) {
+ signatureV3Generator.generate().encode(bOut);
+ } else {
+ signatureGenerator.generate().encode(bOut);
+ }
+ armorOut.close();
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ }
+
+ public static void generateSignature(Context context, InputData data, OutputStream outStream,
+ boolean armored, boolean binary, long signatureKeyId, String signaturePassPhrase,
+ int hashAlgorithm, boolean forceV3Signature, ProgressDialogUpdater progress)
+ throws GeneralException, PGPException, IOException, NoSuchAlgorithmException,
+ SignatureException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ ArmoredOutputStream armorOut = null;
+ OutputStream out = null;
+ if (armored) {
+ armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+ out = armorOut;
+ } else {
+ out = outStream;
+ }
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (signatureKeyId == 0) {
+ throw new GeneralException(context.getString(R.string.error_noSignatureKey));
+ }
+
+ signingKeyRing = getSecretKeyRing(signatureKeyId);
+ signingKey = getSigningKey(signatureKeyId);
+ if (signingKey == null) {
+ throw new GeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new GeneralException(context.getString(R.string.error_noSignaturePassPhrase));
+ }
+ signaturePrivateKey = signingKey.extractPrivateKey(signaturePassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ if (signaturePrivateKey == null) {
+ throw new GeneralException(context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingStreams, 0, 100);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingSignature, 30, 100);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ int type = PGPSignature.CANONICAL_TEXT_DOCUMENT;
+ if (binary) {
+ type = PGPSignature.BINARY_DOCUMENT;
+ }
+
+ if (forceV3Signature) {
+ signatureV3Generator = new PGPV3SignatureGenerator(signingKey.getPublicKey()
+ .getAlgorithm(), hashAlgorithm, new BouncyCastleProvider());
+ signatureV3Generator.initSign(type, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(
+ signingKey.getPublicKey().getAlgorithm(), hashAlgorithm,
+ new BouncyCastleProvider());
+ signatureGenerator.initSign(type, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ String userId = getMainUserId(getMasterKey(signingKeyRing));
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_signing, 40, 100);
+
+ InputStream inStream = data.getInputStream();
+ if (binary) {
+ byte[] buffer = new byte[1 << 16];
+ int n = 0;
+ while ((n = inStream.read(buffer)) > 0) {
+ if (forceV3Signature) {
+ signatureV3Generator.update(buffer, 0, n);
+ } else {
+ signatureGenerator.update(buffer, 0, n);
+ }
+ }
+ } else {
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ while (true) {
+ final String line = reader.readLine();
+
+ if (line == null) {
+ break;
+ }
+
+ if (forceV3Signature) {
+ processLine(line, null, signatureV3Generator);
+ signatureV3Generator.update(newline);
+ } else {
+ processLine(line, null, signatureGenerator);
+ signatureGenerator.update(newline);
+ }
+ }
+ }
+
+ BCPGOutputStream bOut = new BCPGOutputStream(out);
+ if (forceV3Signature) {
+ signatureV3Generator.generate().encode(bOut);
+ } else {
+ signatureGenerator.generate().encode(bOut);
+ }
+ out.close();
+ outStream.close();
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ }
+
+ public static long getDecryptionKeyId(Context context, InputData data) throws GeneralException,
+ NoAsymmetricEncryptionException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(data.getInputStream());
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ // the first object might be a PGP marker packet.
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new GeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ // TODO: currently we always only look at the first known key
+ // find the secret key
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ boolean gotAsymmetricEncryption = false;
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ gotAsymmetricEncryption = true;
+ PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj;
+ secretKey = getSecretKey(pbe.getKeyID());
+ if (secretKey != null) {
+ break;
+ }
+ }
+ }
+
+ if (!gotAsymmetricEncryption) {
+ throw new NoAsymmetricEncryptionException();
+ }
+
+ if (secretKey == null) {
+ return Id.key.none;
+ }
+
+ return secretKey.getKeyID();
+ }
+
+ public static boolean hasSymmetricEncryption(Context context, InputData data)
+ throws GeneralException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(data.getInputStream());
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ // the first object might be a PGP marker packet.
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new GeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPBEEncryptedData) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static Bundle decrypt(Context context, InputData data, OutputStream outStream,
+ String passPhrase, ProgressDialogUpdater progress, boolean assumeSymmetric)
+ throws IOException, GeneralException, PGPException, SignatureException {
+ if (passPhrase == null) {
+ passPhrase = "";
+ }
+ Bundle returnData = new Bundle();
+ InputStream in = PGPUtil.getDecoderStream(data.getInputStream());
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+ long signatureKeyId = 0;
+
+ int currentProgress = 0;
+ if (progress != null)
+ progress.setProgress(R.string.progress_readingData, currentProgress, 100);
+
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new GeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ InputStream clear = null;
+ PGPEncryptedData encryptedData = null;
+
+ currentProgress += 5;
+
+ // TODO: currently we always only look at the first known key or symmetric encryption,
+ // there might be more...
+ if (assumeSymmetric) {
+ PGPPBEEncryptedData pbe = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ // find secret key
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPBEEncryptedData) {
+ pbe = (PGPPBEEncryptedData) obj;
+ break;
+ }
+ }
+
+ if (pbe == null) {
+ throw new GeneralException(
+ context.getString(R.string.error_noSymmetricEncryptionPacket));
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingStreams, currentProgress, 100);
+ clear = pbe.getDataStream(passPhrase.toCharArray(), new BouncyCastleProvider());
+ encryptedData = pbe;
+ currentProgress += 5;
+ } else {
+ if (progress != null)
+ progress.setProgress(R.string.progress_findingKey, currentProgress, 100);
+ PGPPublicKeyEncryptedData pbe = null;
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ // find secret key
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
+ secretKey = getSecretKey(encData.getKeyID());
+ if (secretKey != null) {
+ pbe = encData;
+ break;
+ }
+ }
+ }
+
+ if (secretKey == null) {
+ throw new GeneralException(context.getString(R.string.error_noSecretKeyFound));
+ }
+
+ currentProgress += 5;
+ if (progress != null)
+ progress.setProgress(R.string.progress_extractingKey, currentProgress, 100);
+ PGPPrivateKey privateKey = null;
+ try {
+ privateKey = secretKey.extractPrivateKey(passPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ } catch (PGPException e) {
+ throw new PGPException(context.getString(R.string.error_wrongPassPhrase));
+ }
+ if (privateKey == null) {
+ throw new GeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ currentProgress += 5;
+ if (progress != null)
+ progress.setProgress(R.string.progress_preparingStreams, currentProgress, 100);
+ clear = pbe.getDataStream(privateKey, new BouncyCastleProvider());
+ encryptedData = pbe;
+ currentProgress += 5;
+ }
+
+ PGPObjectFactory plainFact = new PGPObjectFactory(clear);
+ Object dataChunk = plainFact.nextObject();
+ PGPOnePassSignature signature = null;
+ PGPPublicKey signatureKey = null;
+ int signatureIndex = -1;
+
+ if (dataChunk instanceof PGPCompressedData) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_decompressingData, currentProgress, 100);
+ PGPObjectFactory fact = new PGPObjectFactory(
+ ((PGPCompressedData) dataChunk).getDataStream());
+ dataChunk = fact.nextObject();
+ plainFact = fact;
+ currentProgress += 10;
+ }
+
+ if (dataChunk instanceof PGPOnePassSignatureList) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_processingSignature, currentProgress, 100);
+ returnData.putBoolean(EXTRA_SIGNATURE, true);
+ PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = getPublicKey(signature.getKeyID());
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureIndex = i;
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing sigKeyRing = getPublicKeyRing(signatureKeyId);
+ if (sigKeyRing != null) {
+ userId = getMainUserId(getMasterKey(sigKeyRing));
+ }
+ returnData.putString(EXTRA_SIGNATURE_USER_ID, userId);
+ break;
+ }
+ }
+
+ returnData.putLong(EXTRA_SIGNATURE_KEY_ID, signatureKeyId);
+
+ if (signature != null) {
+ signature.initVerify(signatureKey, new BouncyCastleProvider());
+ } else {
+ returnData.putBoolean(EXTRA_SIGNATURE_UNKNOWN, true);
+ }
+
+ dataChunk = plainFact.nextObject();
+ currentProgress += 10;
+ }
+
+ if (dataChunk instanceof PGPSignatureList) {
+ dataChunk = plainFact.nextObject();
+ }
+
+ if (dataChunk instanceof PGPLiteralData) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_decrypting, currentProgress, 100);
+ PGPLiteralData literalData = (PGPLiteralData) dataChunk;
+ OutputStream out = outStream;
+
+ byte[] buffer = new byte[1 << 16];
+ InputStream dataIn = literalData.getInputStream();
+
+ int startProgress = currentProgress;
+ int endProgress = 100;
+ if (signature != null) {
+ endProgress = 90;
+ } else if (encryptedData.isIntegrityProtected()) {
+ endProgress = 95;
+ }
+ int n = 0;
+ int done = 0;
+ long startPos = data.getStreamPosition();
+ while ((n = dataIn.read(buffer)) > 0) {
+ out.write(buffer, 0, n);
+ done += n;
+ if (signature != null) {
+ try {
+ signature.update(buffer, 0, n);
+ } catch (SignatureException e) {
+ returnData.putBoolean(EXTRA_SIGNATURE_SUCCESS, false);
+ signature = null;
+ }
+ }
+ // unknown size, but try to at least have a moving, slowing down progress bar
+ currentProgress = startProgress + (endProgress - startProgress) * done
+ / (done + 100000);
+ if (data.getSize() - startPos == 0) {
+ currentProgress = endProgress;
+ } else {
+ currentProgress = (int) (startProgress + (endProgress - startProgress)
+ * (data.getStreamPosition() - startPos) / (data.getSize() - startPos));
+ }
+ if (progress != null)
+ progress.setProgress(currentProgress, 100);
+ }
+
+ if (signature != null) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_verifyingSignature, 90, 100);
+ PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
+ PGPSignature messageSignature = signatureList.get(signatureIndex);
+ if (signature.verify(messageSignature)) {
+ returnData.putBoolean(EXTRA_SIGNATURE_SUCCESS, true);
+ } else {
+ returnData.putBoolean(EXTRA_SIGNATURE_SUCCESS, false);
+ }
+ }
+ }
+
+ // TODO: add integrity somewhere
+ if (encryptedData.isIntegrityProtected()) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_verifyingIntegrity, 95, 100);
+ if (encryptedData.verify()) {
+ // passed
+ } else {
+ // failed
+ }
+ } else {
+ // no integrity check
+ }
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ public static Bundle verifyText(BaseActivity context, InputData data, OutputStream outStream,
+ ProgressDialogUpdater progress) throws IOException, GeneralException, PGPException,
+ SignatureException {
+ Bundle returnData = new Bundle();
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ArmoredInputStream aIn = new ArmoredInputStream(data.getInputStream());
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 0, 100);
+
+ // mostly taken from ClearSignedFileProcessor
+ ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
+ int lookAhead = readInputLine(lineOut, aIn);
+ byte[] lineSep = getLineSeparator();
+
+ byte[] line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+
+ while (lookAhead != -1 && aIn.isClearText()) {
+ lookAhead = readInputLine(lineOut, lookAhead, aIn);
+ line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+ }
+
+ out.close();
+
+ byte[] clearText = out.toByteArray();
+ outStream.write(clearText);
+
+ returnData.putBoolean(EXTRA_SIGNATURE, true);
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_processingSignature, 60, 100);
+ PGPObjectFactory pgpFact = new PGPObjectFactory(aIn);
+
+ PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject();
+ if (sigList == null) {
+ throw new GeneralException(context.getString(R.string.error_corruptData));
+ }
+ PGPSignature signature = null;
+ long signatureKeyId = 0;
+ PGPPublicKey signatureKey = null;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = getPublicKey(signature.getKeyID());
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ if (signatureKey == null) {
+ Bundle pauseData = new Bundle();
+ pauseData.putInt(Constants.extras.STATUS, Id.message.unknown_signature_key);
+ pauseData.putLong(Constants.extras.KEY_ID, signatureKeyId);
+ Message msg = new Message();
+ msg.setData(pauseData);
+ context.sendMessage(msg);
+ // pause here
+ context.getRunningThread().pause();
+ // see whether the key was found in the meantime
+ signatureKey = getPublicKey(signature.getKeyID());
+ }
+
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing sigKeyRing = getPublicKeyRing(signatureKeyId);
+ if (sigKeyRing != null) {
+ userId = getMainUserId(getMasterKey(sigKeyRing));
+ }
+ returnData.putString(EXTRA_SIGNATURE_USER_ID, userId);
+ break;
+ }
+ }
+
+ returnData.putLong(EXTRA_SIGNATURE_KEY_ID, signatureKeyId);
+
+ if (signature == null) {
+ returnData.putBoolean(EXTRA_SIGNATURE_UNKNOWN, true);
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ signature.initVerify(signatureKey, new BouncyCastleProvider());
+
+ InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText));
+
+ lookAhead = readInputLine(lineOut, sigIn);
+
+ processLine(signature, lineOut.toByteArray());
+
+ if (lookAhead != -1) {
+ do {
+ lookAhead = readInputLine(lineOut, lookAhead, sigIn);
+
+ signature.update((byte) '\r');
+ signature.update((byte) '\n');
+
+ processLine(signature, lineOut.toByteArray());
+ } while (lookAhead != -1);
+ }
+
+ returnData.putBoolean(EXTRA_SIGNATURE_SUCCESS, signature.verify());
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ public static int getStreamContent(Context context, InputStream inStream) throws IOException {
+ InputStream in = PGPUtil.getDecoderStream(inStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ Object object = pgpF.nextObject();
+ while (object != null) {
+ if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) {
+ return Id.content.keys;
+ } else if (object instanceof PGPEncryptedDataList) {
+ return Id.content.encrypted_data;
+ }
+ object = pgpF.nextObject();
+ }
+
+ return Id.content.unknown;
+ }
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPSignatureGenerator pSignatureGenerator) throws IOException, SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPV3SignatureGenerator pSignatureGenerator) throws IOException,
+ SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+ // taken from ClearSignedFileProcessor in BC
+ private static void processLine(PGPSignature sig, byte[] line) throws SignatureException,
+ IOException {
+ int length = getLengthWithoutWhiteSpace(line);
+ if (length > 0) {
+ sig.update(line, 0, length);
+ }
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int lookAhead = -1;
+ int ch;
+
+ while ((ch = fIn.read()) >= 0) {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ }
+
+ return lookAhead;
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int ch = lookAhead;
+
+ do {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ } while ((ch = fIn.read()) >= 0);
+
+ if (ch < 0) {
+ lookAhead = -1;
+ }
+
+ return lookAhead;
+ }
+
+ private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
+ throws IOException {
+ int lookAhead = fIn.read();
+
+ if (lastCh == '\r' && lookAhead == '\n') {
+ bOut.write(lookAhead);
+ lookAhead = fIn.read();
+ }
+
+ return lookAhead;
+ }
+
+ private static int getLengthWithoutSeparator(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isLineEnding(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isLineEnding(byte b) {
+ return b == '\r' || b == '\n';
+ }
+
+ private static int getLengthWithoutWhiteSpace(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isWhiteSpace(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isWhiteSpace(byte b) {
+ return b == '\r' || b == '\n' || b == '\t' || b == ' ';
+ }
+
+ private static byte[] getLineSeparator() {
+ String nl = System.getProperty("line.separator");
+ byte[] nlBytes = new byte[nl.length()];
+
+ for (int i = 0; i != nlBytes.length; i++) {
+ nlBytes[i] = (byte) nl.charAt(i);
+ }
+
+ return nlBytes;
+ }
+
+ public static boolean isReleaseVersion(Context context) {
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(mApgPackageName, 0);
+ if (pi.versionCode % 100 == 99) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (NameNotFoundException e) {
+ // unpossible!
+ return false;
+ }
+ }
+
+ public static String getVersion(Context context) {
+ if (VERSION != null) {
+ return VERSION;
+ }
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(mApgPackageName, 0);
+ VERSION = pi.versionName;
+ return VERSION;
+ } catch (NameNotFoundException e) {
+ // unpossible!
+ return "0.0.0";
+ }
+ }
+
+ public static String getFullVersion(Context context) {
+ return "APG v" + getVersion(context);
+ }
+
+ public static String generateRandomString(int length) {
+ SecureRandom random = new SecureRandom();
+ /*
+ * try { random = SecureRandom.getInstance("SHA1PRNG", new BouncyCastleProvider()); } catch
+ * (NoSuchAlgorithmException e) { // TODO: need to handle this case somehow return null; }
+ */
+ byte bytes[] = new byte[length];
+ random.nextBytes(bytes);
+ String result = "";
+ for (int i = 0; i < length; ++i) {
+ int v = (bytes[i] + 256) % 64;
+ if (v < 10) {
+ result += (char) ('0' + v);
+ } else if (v < 36) {
+ result += (char) ('A' + v - 10);
+ } else if (v < 62) {
+ result += (char) ('a' + v - 36);
+ } else if (v == 62) {
+ result += '_';
+ } else if (v == 63) {
+ result += '.';
+ }
+ }
+ return result;
+ }
+
+ static long getLengthOfStream(InputStream in) throws IOException {
+ long size = 0;
+ long n = 0;
+ byte dummy[] = new byte[0x10000];
+ while ((n = in.read(dummy)) > 0) {
+ size += n;
+ }
+ return size;
+ }
+
+ public static void deleteFileSecurely(Context context, File file, ProgressDialogUpdater progress)
+ throws FileNotFoundException, IOException {
+ long length = file.length();
+ SecureRandom random = new SecureRandom();
+ RandomAccessFile raf = new RandomAccessFile(file, "rws");
+ raf.seek(0);
+ raf.getFilePointer();
+ byte[] data = new byte[1 << 16];
+ int pos = 0;
+ String msg = context.getString(R.string.progress_deletingSecurely, file.getName());
+ while (pos < length) {
+ if (progress != null)
+ progress.setProgress(msg, (int) (100 * pos / length), 100);
+ random.nextBytes(data);
+ raf.write(data);
+ pos += data.length;
+ }
+ raf.close();
+ file.delete();
+ }
+}
diff --git a/org_apg/src/org/apg/ApgService.java b/org_apg/src/org/apg/ApgService.java
new file mode 100644
index 000000000..82e85371d
--- /dev/null
+++ b/org_apg/src/org/apg/ApgService.java
@@ -0,0 +1,643 @@
+package org.apg;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.IApgService;
+
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+public class ApgService extends Service {
+ private final static String TAG = "ApgService";
+ public static final boolean LOCAL_LOGV = true;
+ public static final boolean LOCAL_LOGD = true;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (LOCAL_LOGD)
+ Log.d(TAG, "bound");
+ return mBinder;
+ }
+
+ /** error status */
+ private static enum error {
+ ARGUMENTS_MISSING, APG_FAILURE, NO_MATCHING_SECRET_KEY, PRIVATE_KEY_PASSPHRASE_WRONG, PRIVATE_KEY_PASSPHRASE_MISSING;
+
+ public int shiftedOrdinal() {
+ return ordinal() + 100;
+ }
+ }
+
+ private static enum call {
+ encrypt_with_passphrase, encrypt_with_public_key, decrypt, get_keys
+ }
+
+ /** all arguments that can be passed by calling application */
+ public static enum arg {
+ MESSAGE, // message to encrypt or to decrypt
+ SYMMETRIC_PASSPHRASE, // key for symmetric en/decryption
+ PUBLIC_KEYS, // public keys for encryption
+ ENCRYPTION_ALGORYTHM, // encryption algorithm
+ HASH_ALGORYTHM, // hash algorithm
+ ARMORED_OUTPUT, // whether to armor output
+ FORCE_V3_SIGNATURE, // whether to force v3 signature
+ COMPRESSION, // what compression to use for encrypted output
+ SIGNATURE_KEY, // key for signing
+ PRIVATE_KEY_PASSPHRASE, // passphrase for encrypted private key
+ KEY_TYPE, // type of key (private or public)
+ BLOB, // blob passed
+ }
+
+ /** all things that might be returned */
+ private static enum ret {
+ ERRORS, // string array list with errors
+ WARNINGS, // string array list with warnings
+ ERROR, // numeric error
+ RESULT, // en-/decrypted
+ FINGERPRINTS, // fingerprints of keys
+ USER_IDS, // user ids
+ }
+
+ /** required arguments for each AIDL function */
+ private static final HashMap<String, HashSet<arg>> FUNCTIONS_REQUIRED_ARGS = new HashMap<String, HashSet<arg>>();
+ static {
+ HashSet<arg> args = new HashSet<arg>();
+ args.add(arg.SYMMETRIC_PASSPHRASE);
+ FUNCTIONS_REQUIRED_ARGS.put(call.encrypt_with_passphrase.name(), args);
+
+ args = new HashSet<arg>();
+ args.add(arg.PUBLIC_KEYS);
+ FUNCTIONS_REQUIRED_ARGS.put(call.encrypt_with_public_key.name(), args);
+
+ args = new HashSet<arg>();
+ FUNCTIONS_REQUIRED_ARGS.put(call.decrypt.name(), args);
+
+ args = new HashSet<arg>();
+ args.add(arg.KEY_TYPE);
+ FUNCTIONS_REQUIRED_ARGS.put(call.get_keys.name(), args);
+ }
+
+ /** optional arguments for each AIDL function */
+ private static final HashMap<String, HashSet<arg>> FUNCTIONS_OPTIONAL_ARGS = new HashMap<String, HashSet<arg>>();
+ static {
+ HashSet<arg> args = new HashSet<arg>();
+ args.add(arg.ENCRYPTION_ALGORYTHM);
+ args.add(arg.HASH_ALGORYTHM);
+ args.add(arg.ARMORED_OUTPUT);
+ args.add(arg.FORCE_V3_SIGNATURE);
+ args.add(arg.COMPRESSION);
+ args.add(arg.PRIVATE_KEY_PASSPHRASE);
+ args.add(arg.SIGNATURE_KEY);
+ args.add(arg.BLOB);
+ args.add(arg.MESSAGE);
+ FUNCTIONS_OPTIONAL_ARGS.put(call.encrypt_with_passphrase.name(), args);
+ FUNCTIONS_OPTIONAL_ARGS.put(call.encrypt_with_public_key.name(), args);
+
+ args = new HashSet<arg>();
+ args.add(arg.SYMMETRIC_PASSPHRASE);
+ args.add(arg.PUBLIC_KEYS);
+ args.add(arg.PRIVATE_KEY_PASSPHRASE);
+ args.add(arg.MESSAGE);
+ args.add(arg.BLOB);
+ FUNCTIONS_OPTIONAL_ARGS.put(call.decrypt.name(), args);
+ }
+
+ /** a map from ApgService parameters to function calls to get the default */
+ private static final HashMap<arg, String> FUNCTIONS_DEFAULTS = new HashMap<arg, String>();
+ static {
+ FUNCTIONS_DEFAULTS.put(arg.ENCRYPTION_ALGORYTHM, "getDefaultEncryptionAlgorithm");
+ FUNCTIONS_DEFAULTS.put(arg.HASH_ALGORYTHM, "getDefaultHashAlgorithm");
+ FUNCTIONS_DEFAULTS.put(arg.ARMORED_OUTPUT, "getDefaultAsciiArmour");
+ FUNCTIONS_DEFAULTS.put(arg.FORCE_V3_SIGNATURE, "getForceV3Signatures");
+ FUNCTIONS_DEFAULTS.put(arg.COMPRESSION, "getDefaultMessageCompression");
+ }
+
+ /** a map of the default function names to their method */
+ private static final HashMap<String, Method> FUNCTIONS_DEFAULTS_METHODS = new HashMap<String, Method>();
+ static {
+ try {
+ FUNCTIONS_DEFAULTS_METHODS.put("getDefaultEncryptionAlgorithm",
+ Preferences.class.getMethod("getDefaultEncryptionAlgorithm"));
+ FUNCTIONS_DEFAULTS_METHODS.put("getDefaultHashAlgorithm",
+ Preferences.class.getMethod("getDefaultHashAlgorithm"));
+ FUNCTIONS_DEFAULTS_METHODS.put("getDefaultAsciiArmour",
+ Preferences.class.getMethod("getDefaultAsciiArmour"));
+ FUNCTIONS_DEFAULTS_METHODS.put("getForceV3Signatures",
+ Preferences.class.getMethod("getForceV3Signatures"));
+ FUNCTIONS_DEFAULTS_METHODS.put("getDefaultMessageCompression",
+ Preferences.class.getMethod("getDefaultMessageCompression"));
+ } catch (Exception e) {
+ Log.e(TAG, "Function method exception: " + e.getMessage());
+ }
+ }
+
+ private static void writeToOutputStream(InputStream is, OutputStream os) throws IOException {
+ byte[] buffer = new byte[8];
+ int len = 0;
+ while ((len = is.read(buffer)) != -1) {
+ os.write(buffer, 0, len);
+ }
+ }
+
+ private static Cursor getKeyEntries(HashMap<String, Object> pParams) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " + "("
+ + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + Keys.TABLE_NAME + "."
+ + Keys.KEY_RING_ID + " AND " + Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY
+ + " = '1'" + ") " + " INNER JOIN " + UserIds.TABLE_NAME + " ON " + "("
+ + Keys.TABLE_NAME + "." + Keys._ID + " = " + UserIds.TABLE_NAME + "."
+ + UserIds.KEY_ID + " AND " + UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ String orderBy = pParams.containsKey("order_by") ? (String) pParams.get("order_by")
+ : UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC";
+
+ String typeVal[] = null;
+ String typeWhere = null;
+ if (pParams.containsKey("key_type")) {
+ typeWhere = KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?";
+ typeVal = new String[] { "" + pParams.get("key_type") };
+ }
+ return qb.query(Apg.getDatabase().db(), (String[]) pParams.get("columns"), typeWhere,
+ typeVal, null, null, orderBy);
+ }
+
+ /**
+ * maps a fingerprint or user id of a key to a master key in database
+ *
+ * @param search_key
+ * fingerprint or user id to search for
+ * @return master key if found, or 0
+ */
+ private static long getMasterKey(String pSearchKey, Bundle pReturn) {
+ if (pSearchKey == null || pSearchKey.length() != 8) {
+ return 0;
+ }
+ ArrayList<String> keyList = new ArrayList<String>();
+ keyList.add(pSearchKey);
+ long[] keys = getMasterKey(keyList, pReturn);
+ if (keys.length > 0) {
+ return keys[0];
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * maps fingerprints or user ids of keys to master keys in database
+ *
+ * @param search_keys
+ * a list of keys (fingerprints or user ids) to look for in database
+ * @return an array of master keys
+ */
+ private static long[] getMasterKey(ArrayList<String> pSearchKeys, Bundle pReturn) {
+
+ HashMap<String, Object> qParams = new HashMap<String, Object>();
+ qParams.put("columns", new String[] { KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 0
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 1
+ });
+ qParams.put("key_type", Id.database.type_public);
+
+ Cursor mCursor = getKeyEntries(qParams);
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "going through installed user keys");
+ ArrayList<Long> masterKeys = new ArrayList<Long>();
+ while (mCursor.moveToNext()) {
+ long curMkey = mCursor.getLong(0);
+ String curUser = mCursor.getString(1);
+
+ String curFprint = Apg.getSmallFingerPrint(curMkey);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "current user: " + curUser + " (" + curFprint + ")");
+ if (pSearchKeys.contains(curFprint) || pSearchKeys.contains(curUser)) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "master key found for: " + curFprint);
+ masterKeys.add(curMkey);
+ pSearchKeys.remove(curFprint);
+ } else {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Installed key " + curFprint
+ + " is not in the list of public keys to encrypt with");
+ }
+ }
+ mCursor.close();
+
+ long[] masterKeyLongs = new long[masterKeys.size()];
+ int i = 0;
+ for (Long key : masterKeys) {
+ masterKeyLongs[i++] = key;
+ }
+
+ if (i == 0) {
+ Log.w(TAG, "Found not one public key");
+ pReturn.getStringArrayList(ret.WARNINGS.name()).add(
+ "Searched for public key(s) but found not one");
+ }
+
+ for (String key : pSearchKeys) {
+ Log.w(TAG, "Searched for key " + key + " but cannot find it in APG");
+ pReturn.getStringArrayList(ret.WARNINGS.name()).add(
+ "Searched for key " + key + " but cannot find it in APG");
+ }
+
+ return masterKeyLongs;
+ }
+
+ /**
+ * Add default arguments if missing
+ *
+ * @param args
+ * the bundle to add default parameters to if missing
+ */
+ private void addDefaultArguments(String pCall, Bundle pArgs) {
+ // check whether there are optional elements defined for that call
+ if (FUNCTIONS_OPTIONAL_ARGS.containsKey(pCall)) {
+ Preferences preferences = Preferences.getPreferences(getBaseContext(), true);
+
+ Iterator<arg> iter = FUNCTIONS_DEFAULTS.keySet().iterator();
+ while (iter.hasNext()) {
+ arg currentArg = iter.next();
+ String currentKey = currentArg.name();
+ if (!pArgs.containsKey(currentKey)
+ && FUNCTIONS_OPTIONAL_ARGS.get(pCall).contains(currentArg)) {
+ String currentFunctionName = FUNCTIONS_DEFAULTS.get(currentArg);
+ try {
+ Class<?> returnType = FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName)
+ .getReturnType();
+ if (returnType == String.class) {
+ pArgs.putString(currentKey,
+ (String) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName)
+ .invoke(preferences));
+ } else if (returnType == boolean.class) {
+ pArgs.putBoolean(currentKey,
+ (Boolean) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName)
+ .invoke(preferences));
+ } else if (returnType == int.class) {
+ pArgs.putInt(currentKey,
+ (Integer) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName)
+ .invoke(preferences));
+ } else {
+ Log.e(TAG, "Unknown return type " + returnType.toString()
+ + " for default option");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in add_default_arguments " + e.getMessage());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * updates a Bundle with default return values
+ *
+ * @param pReturn
+ * the Bundle to update
+ */
+ private void addDefaultReturns(Bundle pReturn) {
+ ArrayList<String> errors = new ArrayList<String>();
+ ArrayList<String> warnings = new ArrayList<String>();
+
+ pReturn.putStringArrayList(ret.ERRORS.name(), errors);
+ pReturn.putStringArrayList(ret.WARNINGS.name(), warnings);
+ }
+
+ /**
+ * checks for required arguments and adds them to the error if missing
+ *
+ * @param function
+ * the functions required arguments to check for
+ * @param pArgs
+ * the Bundle of arguments to check
+ * @param pReturn
+ * the bundle to write errors to
+ */
+ private void checkForRequiredArgs(String pFunction, Bundle pArgs, Bundle pReturn) {
+ if (FUNCTIONS_REQUIRED_ARGS.containsKey(pFunction)) {
+ Iterator<arg> iter = FUNCTIONS_REQUIRED_ARGS.get(pFunction).iterator();
+ while (iter.hasNext()) {
+ String curArg = iter.next().name();
+ if (!pArgs.containsKey(curArg)) {
+ pReturn.getStringArrayList(ret.ERRORS.name())
+ .add("Argument missing: " + curArg);
+ }
+ }
+ }
+
+ if (pFunction.equals(call.encrypt_with_passphrase.name())
+ || pFunction.equals(call.encrypt_with_public_key.name())
+ || pFunction.equals(call.decrypt.name())) {
+ // check that either MESSAGE or BLOB are there
+ if (!pArgs.containsKey(arg.MESSAGE.name()) && !pArgs.containsKey(arg.BLOB.name())) {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Arguments missing: Neither MESSAGE nor BLOG found");
+ }
+
+ }
+ }
+
+ /**
+ * checks for unknown arguments and add them to warning if found
+ *
+ * @param function
+ * the functions name to check against
+ * @param pArgs
+ * the Bundle of arguments to check
+ * @param pReturn
+ * the bundle to write warnings to
+ */
+ private void checkForUnknownArgs(String pFunction, Bundle pArgs, Bundle pReturn) {
+
+ HashSet<arg> allArgs = new HashSet<arg>();
+ if (FUNCTIONS_REQUIRED_ARGS.containsKey(pFunction)) {
+ allArgs.addAll(FUNCTIONS_REQUIRED_ARGS.get(pFunction));
+ }
+ if (FUNCTIONS_OPTIONAL_ARGS.containsKey(pFunction)) {
+ allArgs.addAll(FUNCTIONS_OPTIONAL_ARGS.get(pFunction));
+ }
+
+ ArrayList<String> unknownArgs = new ArrayList<String>();
+ Iterator<String> iter = pArgs.keySet().iterator();
+ while (iter.hasNext()) {
+ String curKey = iter.next();
+ try {
+ arg curArg = arg.valueOf(curKey);
+ if (!allArgs.contains(curArg)) {
+ pReturn.getStringArrayList(ret.WARNINGS.name()).add(
+ "Unknown argument: " + curKey);
+ unknownArgs.add(curKey);
+ }
+ } catch (Exception e) {
+ pReturn.getStringArrayList(ret.WARNINGS.name()).add("Unknown argument: " + curKey);
+ unknownArgs.add(curKey);
+ }
+ }
+
+ // remove unknown arguments so our bundle has just what we need
+ for (String arg : unknownArgs) {
+ pArgs.remove(arg);
+ }
+ }
+
+ private boolean prepareArgs(String pCall, Bundle pArgs, Bundle pReturn) {
+ Apg.initialize(getBaseContext());
+
+ /* add default return values for all functions */
+ addDefaultReturns(pReturn);
+
+ /* add default arguments if missing */
+ addDefaultArguments(pCall, pArgs);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "add_default_arguments");
+
+ /* check for required arguments */
+ checkForRequiredArgs(pCall, pArgs, pReturn);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "check_required_args");
+
+ /* check for unknown arguments and add to warning if found */
+ checkForUnknownArgs(pCall, pArgs, pReturn);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "check_unknown_args");
+
+ /* return if errors happened */
+ if (pReturn.getStringArrayList(ret.ERRORS.name()).size() != 0) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Errors after preparing, not executing " + pCall);
+ pReturn.putInt(ret.ERROR.name(), error.ARGUMENTS_MISSING.shiftedOrdinal());
+ return false;
+ }
+ if (LOCAL_LOGV)
+ Log.v(TAG, "error return");
+
+ return true;
+ }
+
+ private boolean encrypt(Bundle pArgs, Bundle pReturn) {
+ boolean isBlob = pArgs.containsKey(arg.BLOB.name());
+
+ long pubMasterKeys[] = {};
+ if (pArgs.containsKey(arg.PUBLIC_KEYS.name())) {
+ ArrayList<String> list = pArgs.getStringArrayList(arg.PUBLIC_KEYS.name());
+ ArrayList<String> pubKeys = new ArrayList<String>();
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Long size: " + list.size());
+ Iterator<String> iter = list.iterator();
+ while (iter.hasNext()) {
+ pubKeys.add(iter.next());
+ }
+ pubMasterKeys = getMasterKey(pubKeys, pReturn);
+ }
+
+ InputStream inStream = null;
+ if (isBlob) {
+ ContentResolver cr = getContentResolver();
+ try {
+ inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name())));
+ } catch (Exception e) {
+ Log.e(TAG, "... exception on opening blob", e);
+ }
+ } else {
+ inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes());
+ }
+ InputData in = new InputData(inStream, 0); // XXX Size second param?
+
+ OutputStream out = new ByteArrayOutputStream();
+ if (LOCAL_LOGV)
+ Log.v(TAG, "About to encrypt");
+ try {
+ Apg.encrypt(getBaseContext(), // context
+ in, // input stream
+ out, // output stream
+ pArgs.getBoolean(arg.ARMORED_OUTPUT.name()), // ARMORED_OUTPUT
+ pubMasterKeys, // encryption keys
+ getMasterKey(pArgs.getString(arg.SIGNATURE_KEY.name()), pReturn), // signature
+ // key
+ pArgs.getString(arg.PRIVATE_KEY_PASSPHRASE.name()), // signature passphrase
+ null, // progress
+ pArgs.getInt(arg.ENCRYPTION_ALGORYTHM.name()), // encryption
+ pArgs.getInt(arg.HASH_ALGORYTHM.name()), // hash
+ pArgs.getInt(arg.COMPRESSION.name()), // compression
+ pArgs.getBoolean(arg.FORCE_V3_SIGNATURE.name()), // mPreferences.getForceV3Signatures(),
+ pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) // passPhrase
+ );
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in encrypt");
+ String msg = e.getMessage();
+ if (msg.equals(getBaseContext().getString(R.string.error_noSignaturePassPhrase))) {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Cannot encrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name() + " missing): "
+ + msg);
+ pReturn.putInt(ret.ERROR.name(),
+ error.PRIVATE_KEY_PASSPHRASE_MISSING.shiftedOrdinal());
+ } else if (msg.equals(getBaseContext().getString(
+ R.string.error_couldNotExtractPrivateKey))) {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Cannot encrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name()
+ + " probably wrong): " + msg);
+ pReturn.putInt(ret.ERROR.name(),
+ error.PRIVATE_KEY_PASSPHRASE_WRONG.shiftedOrdinal());
+ } else {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Internal failure (" + e.getClass() + ") in APG when encrypting: "
+ + e.getMessage());
+ pReturn.putInt(ret.ERROR.name(), error.APG_FAILURE.shiftedOrdinal());
+ }
+ return false;
+ }
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Encrypted");
+ if (isBlob) {
+ ContentResolver cr = getContentResolver();
+ try {
+ OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB
+ .name())));
+ writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()), outStream);
+ outStream.close();
+ } catch (Exception e) {
+ Log.e(TAG, "... exception on writing blob", e);
+ }
+ } else {
+ pReturn.putString(ret.RESULT.name(), out.toString());
+ }
+ return true;
+ }
+
+ private final IApgService.Stub mBinder = new IApgService.Stub() {
+
+ public boolean getKeys(Bundle pArgs, Bundle pReturn) {
+
+ prepareArgs("get_keys", pArgs, pReturn);
+
+ HashMap<String, Object> qParams = new HashMap<String, Object>();
+ qParams.put("columns", new String[] {
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 0
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 1
+ });
+
+ qParams.put("key_type", pArgs.getInt(arg.KEY_TYPE.name()));
+
+ Cursor cursor = getKeyEntries(qParams);
+ ArrayList<String> fPrints = new ArrayList<String>();
+ ArrayList<String> ids = new ArrayList<String>();
+ while (cursor.moveToNext()) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "adding key " + Apg.getSmallFingerPrint(cursor.getLong(0)));
+ fPrints.add(Apg.getSmallFingerPrint(cursor.getLong(0)));
+ ids.add(cursor.getString(1));
+ }
+ cursor.close();
+
+ pReturn.putStringArrayList(ret.FINGERPRINTS.name(), fPrints);
+ pReturn.putStringArrayList(ret.USER_IDS.name(), ids);
+ return true;
+ }
+
+ public boolean encryptWithPublicKey(Bundle pArgs, Bundle pReturn) {
+ if (!prepareArgs("encrypt_with_public_key", pArgs, pReturn)) {
+ return false;
+ }
+
+ return encrypt(pArgs, pReturn);
+ }
+
+ public boolean encryptWithPassphrase(Bundle pArgs, Bundle pReturn) {
+ if (!prepareArgs("encrypt_with_passphrase", pArgs, pReturn)) {
+ return false;
+ }
+
+ return encrypt(pArgs, pReturn);
+
+ }
+
+ public boolean decrypt(Bundle pArgs, Bundle pReturn) {
+ if (!prepareArgs("decrypt", pArgs, pReturn)) {
+ return false;
+ }
+
+ boolean isBlob = pArgs.containsKey(arg.BLOB.name());
+
+ String passphrase = pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) != null ? pArgs
+ .getString(arg.SYMMETRIC_PASSPHRASE.name()) : pArgs
+ .getString(arg.PRIVATE_KEY_PASSPHRASE.name());
+
+ InputStream inStream = null;
+ if (isBlob) {
+ ContentResolver cr = getContentResolver();
+ try {
+ inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name())));
+ } catch (Exception e) {
+ Log.e(TAG, "... exception on opening blob", e);
+ }
+ } else {
+ inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes());
+ }
+
+ InputData in = new InputData(inStream, 0); // XXX what size in second parameter?
+ OutputStream out = new ByteArrayOutputStream();
+ if (LOCAL_LOGV)
+ Log.v(TAG, "About to decrypt");
+ try {
+ Apg.decrypt(getBaseContext(), in, out, passphrase, null, // progress
+ pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) != null // symmetric
+ );
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in decrypt");
+ String msg = e.getMessage();
+ if (msg.equals(getBaseContext().getString(R.string.error_noSecretKeyFound))) {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add("Cannot decrypt: " + msg);
+ pReturn.putInt(ret.ERROR.name(), error.NO_MATCHING_SECRET_KEY.shiftedOrdinal());
+ } else if (msg.equals(getBaseContext().getString(R.string.error_wrongPassPhrase))) {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Cannot decrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name()
+ + " wrong/missing): " + msg);
+ pReturn.putInt(ret.ERROR.name(),
+ error.PRIVATE_KEY_PASSPHRASE_WRONG.shiftedOrdinal());
+ } else {
+ pReturn.getStringArrayList(ret.ERRORS.name()).add(
+ "Internal failure (" + e.getClass() + ") in APG when decrypting: "
+ + msg);
+ pReturn.putInt(ret.ERROR.name(), error.APG_FAILURE.shiftedOrdinal());
+ }
+ return false;
+ }
+ if (LOCAL_LOGV)
+ Log.v(TAG, "... decrypted");
+
+ if (isBlob) {
+ ContentResolver cr = getContentResolver();
+ try {
+ OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB
+ .name())));
+ writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()),
+ outStream);
+ outStream.close();
+ } catch (Exception e) {
+ Log.e(TAG, "... exception on writing blob", e);
+ }
+ } else {
+ pReturn.putString(ret.RESULT.name(), out.toString());
+ }
+ return true;
+ }
+
+ };
+}
diff --git a/org_apg/src/org/apg/AskForSecretKeyPassPhrase.java b/org_apg/src/org/apg/AskForSecretKeyPassPhrase.java
new file mode 100644
index 000000000..ebed75667
--- /dev/null
+++ b/org_apg/src/org/apg/AskForSecretKeyPassPhrase.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.apg.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class AskForSecretKeyPassPhrase {
+ public static interface PassPhraseCallbackInterface {
+ void passPhraseCallback(long keyId, String passPhrase);
+ }
+
+ public static Dialog createDialog(Activity context, long secretKeyId,
+ PassPhraseCallbackInterface callback) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(context);
+
+ alert.setTitle(R.string.title_authentication);
+
+ final PGPSecretKey secretKey;
+ final Activity activity = context;
+
+ if (secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none) {
+ secretKey = null;
+ alert.setMessage(context.getString(R.string.passPhraseForSymmetricEncryption));
+ } else {
+ secretKey = Apg.getMasterKey(Apg.getSecretKeyRing(secretKeyId));
+ if (secretKey == null) {
+ alert.setTitle(R.string.title_keyNotFound);
+ alert.setMessage(context.getString(R.string.keyNotFound, secretKeyId));
+ alert.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ activity.removeDialog(Id.dialog.pass_phrase);
+ }
+ });
+ alert.setCancelable(false);
+ return alert.create();
+ }
+ String userId = Apg.getMainUserIdSafe(context, secretKey);
+ alert.setMessage(context.getString(R.string.passPhraseFor, userId));
+ }
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.pass_phrase, null);
+ final EditText input = (EditText) view.findViewById(R.id.passPhrase);
+ final EditText inputNotUsed = (EditText) view.findViewById(R.id.passPhraseAgain);
+ inputNotUsed.setVisibility(View.GONE);
+
+ alert.setView(view);
+
+ final PassPhraseCallbackInterface cb = callback;
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ activity.removeDialog(Id.dialog.pass_phrase);
+ String passPhrase = "" + input.getText();
+ long keyId;
+ if (secretKey != null) {
+ try {
+ PGPPrivateKey testKey = secretKey.extractPrivateKey(
+ passPhrase.toCharArray(), new BouncyCastleProvider());
+ if (testKey == null) {
+ Toast.makeText(activity, R.string.error_couldNotExtractPrivateKey,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (PGPException e) {
+ Toast.makeText(activity, R.string.wrongPassPhrase, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ keyId = secretKey.getKeyID();
+ } else {
+ keyId = Id.key.symmetric;
+ }
+ cb.passPhraseCallback(keyId, passPhrase);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ activity.removeDialog(Id.dialog.pass_phrase);
+ }
+ });
+
+ return alert.create();
+ }
+}
diff --git a/org_apg/src/org/apg/CachedPassPhrase.java b/org_apg/src/org/apg/CachedPassPhrase.java
new file mode 100644
index 000000000..2d67a300d
--- /dev/null
+++ b/org_apg/src/org/apg/CachedPassPhrase.java
@@ -0,0 +1,48 @@
+package org.apg;
+
+public class CachedPassPhrase {
+ public final long timestamp;
+ public final String passPhrase;
+
+ public CachedPassPhrase(long timestamp, String passPhrase) {
+ super();
+ this.timestamp = timestamp;
+ this.passPhrase = passPhrase;
+ }
+
+ @Override
+ public int hashCode() {
+ int hc1 = (int) (this.timestamp & 0xffffffff);
+ int hc2 = (this.passPhrase == null ? 0 : this.passPhrase.hashCode());
+ return (hc1 + hc2) * hc2 + hc1;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof CachedPassPhrase)) {
+ return false;
+ }
+
+ CachedPassPhrase o = (CachedPassPhrase) other;
+ if (timestamp != o.timestamp) {
+ return false;
+ }
+
+ if (passPhrase != o.passPhrase) {
+ if (passPhrase == null || o.passPhrase == null) {
+ return false;
+ }
+
+ if (!passPhrase.equals(o.passPhrase)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + timestamp + ", *******)";
+ }
+}
diff --git a/org_apg/src/org/apg/Constants.java b/org_apg/src/org/apg/Constants.java
new file mode 100644
index 000000000..9669d4b0d
--- /dev/null
+++ b/org_apg/src/org/apg/Constants.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import android.os.Environment;
+
+public final class Constants {
+
+ public static final String TAG = "APG";
+
+ public static final class path {
+ public static final String APP_DIR = Environment.getExternalStorageDirectory() + "/APG";
+ }
+
+ public static final class pref {
+ public static final String HAS_SEEN_HELP = "seenHelp";
+ public static final String HAS_SEEN_CHANGE_LOG = "seenChangeLogDialog";
+ public static final String DEFAULT_ENCRYPTION_ALGORITHM = "defaultEncryptionAlgorithm";
+ public static final String DEFAULT_HASH_ALGORITHM = "defaultHashAlgorithm";
+ public static final String DEFAULT_ASCII_ARMOUR = "defaultAsciiArmour";
+ public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression";
+ public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression";
+ public static final String PASS_PHRASE_CACHE_TTL = "passPhraseCacheTtl";
+ public static final String LANGUAGE = "language";
+ public static final String FORCE_V3_SIGNATURES = "forceV3Signatures";
+ public static final String KEY_SERVERS = "keyServers";
+ }
+
+ public static final class defaults {
+ public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu";
+ }
+
+ public static final class extras {
+ public static final String PROGRESS = "progress";
+ public static final String PROGRESS_MAX = "max";
+ public static final String STATUS = "status";
+ public static final String MESSAGE = "message";
+ public static final String KEY_ID = "keyId";
+ }
+}
diff --git a/org_apg/src/org/apg/DataDestination.java b/org_apg/src/org/apg/DataDestination.java
new file mode 100644
index 000000000..509670e69
--- /dev/null
+++ b/org_apg/src/org/apg/DataDestination.java
@@ -0,0 +1,81 @@
+package org.apg;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apg.Apg.GeneralException;
+import org.apg.R;
+
+import android.content.Context;
+import android.os.Environment;
+
+public class DataDestination {
+ private String mStreamFilename;
+ private String mFilename;
+ private int mMode = Id.mode.undefined;
+
+ public DataDestination() {
+
+ }
+
+ public void setMode(int mode) {
+ mMode = mode;
+ }
+
+ public void setFilename(String filename) {
+ mFilename = filename;
+ }
+
+ public String getStreamFilename() {
+ return mStreamFilename;
+ }
+
+ public OutputStream getOutputStream(Context context) throws Apg.GeneralException,
+ FileNotFoundException, IOException {
+ OutputStream out = null;
+ mStreamFilename = null;
+
+ switch (mMode) {
+ case Id.mode.stream: {
+ try {
+ while (true) {
+ mStreamFilename = Apg.generateRandomString(32);
+ if (mStreamFilename == null) {
+ throw new Apg.GeneralException("couldn't generate random file name");
+ }
+ context.openFileInput(mStreamFilename).close();
+ }
+ } catch (FileNotFoundException e) {
+ // found a name that isn't used yet
+ }
+ out = context.openFileOutput(mStreamFilename, Context.MODE_PRIVATE);
+ break;
+ }
+
+ case Id.mode.byte_array: {
+ out = new ByteArrayOutputStream();
+ break;
+ }
+
+ case Id.mode.file: {
+ if (mFilename.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException(
+ context.getString(R.string.error_externalStorageNotReady));
+ }
+ }
+ out = new FileOutputStream(mFilename);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return out;
+ }
+}
diff --git a/org_apg/src/org/apg/DataSource.java b/org_apg/src/org/apg/DataSource.java
new file mode 100644
index 000000000..340afa9f8
--- /dev/null
+++ b/org_apg/src/org/apg/DataSource.java
@@ -0,0 +1,104 @@
+package org.apg;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apg.Apg.GeneralException;
+import org.apg.R;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+
+public class DataSource {
+ private Uri mContentUri = null;
+ private String mText = null;
+ private byte[] mData = null;
+
+ public DataSource() {
+
+ }
+
+ public void setUri(Uri uri) {
+ mContentUri = uri;
+ mText = null;
+ mData = null;
+ }
+
+ public void setUri(String uri) {
+ if (uri.startsWith("/")) {
+ setUri(Uri.parse("file://" + uri));
+ } else {
+ setUri(Uri.parse(uri));
+ }
+ }
+
+ public void setText(String text) {
+ mText = text;
+ mData = null;
+ mContentUri = null;
+ }
+
+ public void setData(byte[] data) {
+ mData = data;
+ mText = null;
+ mContentUri = null;
+ }
+
+ public boolean isText() {
+ return mText != null;
+ }
+
+ public boolean isBinary() {
+ return mData != null || mContentUri != null;
+ }
+
+ public InputData getInputData(Context context, boolean withSize) throws GeneralException,
+ FileNotFoundException, IOException {
+ InputStream in = null;
+ long size = 0;
+
+ if (mContentUri != null) {
+ if (mContentUri.getScheme().equals("file")) {
+ // get the rest after "file://"
+ String path = Uri.decode(mContentUri.toString().substring(7));
+ if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException(
+ context.getString(R.string.error_externalStorageNotReady));
+ }
+ }
+ in = new FileInputStream(path);
+ File file = new File(path);
+ if (withSize) {
+ size = file.length();
+ }
+ } else {
+ in = context.getContentResolver().openInputStream(mContentUri);
+ if (withSize) {
+ InputStream tmp = context.getContentResolver().openInputStream(mContentUri);
+ size = Apg.getLengthOfStream(tmp);
+ tmp.close();
+ }
+ }
+ } else if (mText != null || mData != null) {
+ byte[] bytes = null;
+ if (mData != null) {
+ bytes = mData;
+ } else {
+ bytes = mText.getBytes();
+ }
+ in = new ByteArrayInputStream(bytes);
+ if (withSize) {
+ size = bytes.length;
+ }
+ }
+
+ return new InputData(in, size);
+ }
+
+}
diff --git a/org_apg/src/org/apg/FileDialog.java b/org_apg/src/org/apg/FileDialog.java
new file mode 100644
index 000000000..6ad498b90
--- /dev/null
+++ b/org_apg/src/org/apg/FileDialog.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import org.apg.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+public class FileDialog {
+ private static EditText mFilename;
+ private static ImageButton mBrowse;
+ private static CheckBox mCheckBox;
+ private static Activity mActivity;
+ private static String mFileManagerTitle;
+ private static String mFileManagerButton;
+ private static int mRequestCode;
+
+ public static interface OnClickListener {
+ public void onCancelClick();
+
+ public void onOkClick(String filename, boolean checkbox);
+ }
+
+ public static AlertDialog build(Activity activity, String title, String message,
+ String defaultFile, OnClickListener onClickListener, String fileManagerTitle,
+ String fileManagerButton, String checkboxText, int requestCode) {
+ // TODO: fileManagerTitle and fileManagerButton are deprecated, no use for them right now,
+ // but maybe the Intent now used will someday support them again, so leaving them in
+ LayoutInflater inflater = (LayoutInflater) activity
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(title);
+ alert.setMessage(message);
+
+ View view = inflater.inflate(R.layout.file_dialog, null);
+
+ mActivity = activity;
+ mFilename = (EditText) view.findViewById(R.id.input);
+ mFilename.setText(defaultFile);
+ mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ openFile();
+ }
+ });
+ mFileManagerTitle = fileManagerTitle;
+ mFileManagerButton = fileManagerButton;
+ mRequestCode = requestCode;
+ mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
+ if (checkboxText == null) {
+ mCheckBox.setEnabled(false);
+ mCheckBox.setVisibility(View.GONE);
+ } else {
+ mCheckBox.setEnabled(true);
+ mCheckBox.setVisibility(View.VISIBLE);
+ mCheckBox.setText(checkboxText);
+ }
+
+ alert.setView(view);
+
+ final OnClickListener clickListener = onClickListener;
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ boolean checked = false;
+ if (mCheckBox.isEnabled()) {
+ checked = mCheckBox.isChecked();
+ }
+ clickListener.onOkClick(mFilename.getText().toString(), checked);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ clickListener.onCancelClick();
+ }
+ });
+ return alert.create();
+ }
+
+ public static void setFilename(String filename) {
+ if (mFilename != null) {
+ mFilename.setText(filename);
+ }
+ }
+
+ /**
+ * Opens the file manager to select a file to open.
+ */
+ private static void openFile() {
+ String filename = mFilename.getText().toString();
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType("*/*");
+
+ try {
+ mActivity.startActivityForResult(intent, mRequestCode);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(mActivity, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/HkpKeyServer.java b/org_apg/src/org/apg/HkpKeyServer.java
new file mode 100644
index 000000000..294a60cb2
--- /dev/null
+++ b/org_apg/src/org/apg/HkpKeyServer.java
@@ -0,0 +1,242 @@
+package org.apg;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+import android.text.Html;
+
+public class HkpKeyServer extends KeyServer {
+ private static class HttpError extends Exception {
+ private static final long serialVersionUID = 1718783705229428893L;
+ private int mCode;
+ private String mData;
+
+ public HttpError(int code, String data) {
+ super("" + code + ": " + data);
+ mCode = code;
+ mData = data;
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getData() {
+ return mData;
+ }
+ }
+
+ private String mHost;
+ private short mPort = 11371;
+
+ // example:
+ // pub 2048R/<a href="/pks/lookup?op=get&search=0x887DF4BE9F5C9090">9F5C9090</a> 2009-08-17 <a
+ // href="/pks/lookup?op=vindex&search=0x887DF4BE9F5C9090">Jörg Runge
+ // &lt;joerg@joergrunge.de&gt;</a>
+ public static Pattern PUB_KEY_LINE = Pattern
+ .compile(
+ "pub +([0-9]+)([a-z]+)/.*?0x([0-9a-z]+).*? +([0-9-]+) +(.+)[\n\r]+((?: +.+[\n\r]+)*)",
+ Pattern.CASE_INSENSITIVE);
+ public static Pattern USER_ID_LINE = Pattern.compile("^ +(.+)$", Pattern.MULTILINE
+ | Pattern.CASE_INSENSITIVE);
+
+ public HkpKeyServer(String host) {
+ mHost = host;
+ }
+
+ public HkpKeyServer(String host, short port) {
+ mHost = host;
+ mPort = port;
+ }
+
+ static private String readAll(InputStream in, String encoding) throws IOException {
+ ByteArrayOutputStream raw = new ByteArrayOutputStream();
+
+ byte buffer[] = new byte[1 << 16];
+ int n = 0;
+ while ((n = in.read(buffer)) != -1) {
+ raw.write(buffer, 0, n);
+ }
+
+ if (encoding == null) {
+ encoding = "utf8";
+ }
+ return raw.toString(encoding);
+ }
+
+ // TODO: replace this with httpclient
+ private String query(String request) throws QueryException, HttpError {
+ InetAddress ips[];
+ try {
+ ips = InetAddress.getAllByName(mHost);
+ } catch (UnknownHostException e) {
+ throw new QueryException(e.toString());
+ }
+ for (int i = 0; i < ips.length; ++i) {
+ try {
+ String url = "http://" + ips[i].getHostAddress() + ":" + mPort + request;
+ URL realUrl = new URL(url);
+ HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(25000);
+ conn.connect();
+ int response = conn.getResponseCode();
+ if (response >= 200 && response < 300) {
+ return readAll(conn.getInputStream(), conn.getContentEncoding());
+ } else {
+ String data = readAll(conn.getErrorStream(), conn.getContentEncoding());
+ throw new HttpError(response, data);
+ }
+ } catch (MalformedURLException e) {
+ // nothing to do, try next IP
+ } catch (IOException e) {
+ // nothing to do, try next IP
+ }
+ }
+
+ throw new QueryException("querying server(s) for '" + mHost + "' failed");
+ }
+
+ // TODO: replace this with httpclient
+ @Override
+ public List<KeyInfo> search(String query) throws QueryException, TooManyResponses,
+ InsufficientQuery {
+ Vector<KeyInfo> results = new Vector<KeyInfo>();
+
+ if (query.length() < 3) {
+ throw new InsufficientQuery();
+ }
+
+ String encodedQuery;
+ try {
+ encodedQuery = URLEncoder.encode(query, "utf8");
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ String request = "/pks/lookup?op=index&search=" + encodedQuery;
+
+ String data = null;
+ try {
+ data = query(request);
+ } catch (HttpError e) {
+ if (e.getCode() == 404) {
+ return results;
+ } else {
+ if (e.getData().toLowerCase().contains("no keys found")) {
+ return results;
+ } else if (e.getData().toLowerCase().contains("too many")) {
+ throw new TooManyResponses();
+ } else if (e.getData().toLowerCase().contains("insufficient")) {
+ throw new InsufficientQuery();
+ }
+ }
+ throw new QueryException("querying server(s) for '" + mHost + "' failed");
+ }
+
+ Matcher matcher = PUB_KEY_LINE.matcher(data);
+ while (matcher.find()) {
+ KeyInfo info = new KeyInfo();
+ info.size = Integer.parseInt(matcher.group(1));
+ info.algorithm = matcher.group(2);
+ info.keyId = Apg.keyFromHex(matcher.group(3));
+ info.fingerPrint = Apg.getSmallFingerPrint(info.keyId);
+ String chunks[] = matcher.group(4).split("-");
+ info.date = new GregorianCalendar(Integer.parseInt(chunks[0]),
+ Integer.parseInt(chunks[1]), Integer.parseInt(chunks[2])).getTime();
+ info.userIds = new Vector<String>();
+ if (matcher.group(5).startsWith("*** KEY")) {
+ info.revoked = matcher.group(5);
+ } else {
+ String tmp = matcher.group(5).replaceAll("<.*?>", "");
+ tmp = Html.fromHtml(tmp).toString();
+ info.userIds.add(tmp);
+ }
+ if (matcher.group(6).length() > 0) {
+ Matcher matcher2 = USER_ID_LINE.matcher(matcher.group(6));
+ while (matcher2.find()) {
+ String tmp = matcher2.group(1).replaceAll("<.*?>", "");
+ tmp = Html.fromHtml(tmp).toString();
+ info.userIds.add(tmp);
+ }
+ }
+ results.add(info);
+ }
+
+ return results;
+ }
+
+ @Override
+ public String get(long keyId) throws QueryException {
+ HttpClient client = new DefaultHttpClient();
+ try {
+ HttpGet get = new HttpGet("http://" + mHost + ":" + mPort
+ + "/pks/lookup?op=get&search=0x" + Apg.keyToHex(keyId));
+
+ HttpResponse response = client.execute(get);
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ throw new QueryException("not found");
+ }
+
+ HttpEntity entity = response.getEntity();
+ InputStream is = entity.getContent();
+ String data = readAll(is, EntityUtils.getContentCharSet(entity));
+ Matcher matcher = Apg.PGP_PUBLIC_KEY.matcher(data);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ } catch (IOException e) {
+ // nothing to do, better luck on the next keyserver
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+
+ return null;
+ }
+
+ @Override
+ void add(String armouredText) throws AddKeyException {
+ HttpClient client = new DefaultHttpClient();
+ try {
+ HttpPost post = new HttpPost("http://" + mHost + ":" + mPort + "/pks/add");
+
+ List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+ nameValuePairs.add(new BasicNameValuePair("keytext", armouredText));
+ post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+
+ HttpResponse response = client.execute(post);
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ throw new AddKeyException();
+ }
+ } catch (IOException e) {
+ // nothing to do, better luck on the next keyserver
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/IApgService.aidl b/org_apg/src/org/apg/IApgService.aidl
new file mode 100644
index 000000000..8101bd2a4
--- /dev/null
+++ b/org_apg/src/org/apg/IApgService.aidl
@@ -0,0 +1,125 @@
+package org.apg;
+
+interface IApgService {
+
+ /* All functions fill the returnVals Bundle with the following keys:
+ *
+ * ArrayList<String> "WARNINGS" = Warnings, if any
+ * ArrayList<String> "ERRORS" = Human readable error descriptions, if any
+ * int "ERROR" = Numeric representation of error, if any
+ * starting with 100:
+ * 100: Required argument missing
+ * 101: Generic failure of APG
+ * 102: No matching private key found
+ * 103: Private key's passphrase wrong
+ * 104: Private key's passphrase missing
+ */
+
+ /* ********************************************************
+ * Encryption
+ * ********************************************************/
+
+ /* All encryption function's arguments
+ *
+ * Bundle params' keys:
+ * (optional/required)
+ * TYPE "STRING KEY" = EXPLANATION / VALUES
+ *
+ * (required)
+ * String "MESSAGE" = Message to encrypt
+ * OR
+ * String "BLOB" = ContentUri to a file handle
+ * with binary data to encrypt
+ * (Attention: file will be overwritten
+ * with encrypted content!)
+ *
+ * (optional)
+ * int "ENCRYPTION_ALGORYTHM" = Encryption Algorithm
+ * 7: AES-128, 8: AES-192, 9: AES-256,
+ * 4: Blowfish, 10: Twofish, 3: CAST5,
+ * 6: DES, 2: Triple DES, 1: IDEA
+ * (optional)
+ * int "HASH_ALGORYTHM" = Hash Algorithm
+ * 1: MD5, 3: RIPEMD-160, 2: SHA-1,
+ * 11: SHA-224, 8: SHA-256, 9: SHA-384,
+ * 10: SHA-512
+ * (optional)
+ * Boolean "ARMORED_OUTPUT" = Armor output
+ *
+ * (optional)
+ * Boolean "FORCE_V3_SIGNATURE" = Force V3 Signatures
+ *
+ * (optional)
+ * int "COMPRESSION" = Compression to use
+ * 0x21070001: none, 1: Zip, 2: Zlib,
+ * 3: BZip2
+ * (optional)
+ * String "SIGNATURE_KEY" = Key to sign with
+ *
+ * (optional)
+ * String "PRIVATE_KEY_PASSPHRASE" = Passphrase for signing key
+ *
+ * Bundle returnVals (in addition to the ERRORS/WARNINGS above):
+ * If "MESSAGE" was set:
+ * String "RESULT" = Encrypted message
+ */
+
+ /* Additional argument for function below:
+ * (required)
+ * String "SYMMETRIC_PASSPHRASE" = Symmetric passphrase to use
+ */
+ boolean encryptWithPassphrase(in Bundle params, out Bundle returnVals);
+
+ /* Additional argument:
+ * (required)
+ * ArrayList<String> "PUBLIC_KEYS" = Public keys (8char fingerprint "123ABC12" OR
+ * complete id "Alice Meyer <ab@email.com>")
+ */
+ boolean encryptWithPublicKey(in Bundle params, out Bundle returnVals);
+
+ /* ********************************************************
+ * Decryption
+ * ********************************************************/
+
+ /* Bundle params:
+ * (required)
+ * String "MESSAGE" = Message to dencrypt
+ * OR
+ * String "BLOB" = ContentUri to a file handle
+ * with binary data to dencrypt
+ * (Attention: file will be overwritten
+ * with dencrypted content!)
+ *
+ * (optional)
+ * String "SYMMETRIC_PASSPHRASE" = Symmetric passphrase for decryption
+ *
+ * (optional)
+ * String "PRIVATE_KEY_PASSPHRASE" = Private keys's passphrase on asymmetric encryption
+ *
+ * Bundle return_vals:
+ * If "MESSAGE" was set:
+ * String "RESULT" = Decrypted message
+ */
+ boolean decrypt(in Bundle params, out Bundle returnVals);
+
+ /* ********************************************************
+ * Get key information
+ * ********************************************************/
+
+ /* Get info about all available keys
+ *
+ * Bundle params:
+ * (required)
+ * int "KEY_TYPE" = info about what type of keys to return
+ * 0: public keys
+ * 1: private keys
+ *
+ * Returns:
+ * StringArrayList "FINGERPRINTS" = Short fingerprints of keys
+ *
+ * StringArrayList "USER_IDS" = User ids of corresponding fingerprints
+ * (order is the same as in FINGERPRINTS)
+ */
+ boolean getKeys(in Bundle params, out Bundle returnVals);
+
+} \ No newline at end of file
diff --git a/org_apg/src/org/apg/Id.java b/org_apg/src/org/apg/Id.java
new file mode 100644
index 000000000..adcad0534
--- /dev/null
+++ b/org_apg/src/org/apg/Id.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+
+public final class Id {
+
+ public static final String TAG = "APG";
+
+ public static final class menu {
+ public static final int export = 0x21070001;
+ public static final int delete = 0x21070002;
+ public static final int edit = 0x21070003;
+ public static final int update = 0x21070004;
+ public static final int exportToServer = 0x21070005;
+ public static final int share = 0x21070006;
+ public static final int signKey = 0x21070007;
+
+ public static final class option {
+ public static final int new_pass_phrase = 0x21070001;
+ public static final int create = 0x21070002;
+ public static final int about = 0x21070003;
+ public static final int manage_public_keys = 0x21070004;
+ public static final int manage_secret_keys = 0x21070005;
+ public static final int import_keys = 0x21070006;
+ public static final int export_keys = 0x21070007;
+ public static final int preferences = 0x21070008;
+ public static final int search = 0x21070009;
+ public static final int help = 0x21070010;
+ public static final int key_server = 0x21070011;
+ public static final int scanQRCode = 0x21070012;
+ }
+ }
+
+ public static final class message {
+ public static final int progress_update = 0x21070001;
+ public static final int done = 0x21070002;
+ public static final int import_keys = 0x21070003;
+ public static final int export_keys = 0x21070004;
+ public static final int import_done = 0x21070005;
+ public static final int export_done = 0x21070006;
+ public static final int create_key = 0x21070007;
+ public static final int edit_key = 0x21070008;
+ public static final int delete_done = 0x21070009;
+ public static final int query_done = 0x21070010;
+ public static final int unknown_signature_key = 0x21070011;
+ }
+
+ public static final class request {
+ public static final int public_keys = 0x21070001;
+ public static final int secret_keys = 0x21070002;
+ public static final int filename = 0x21070003;
+ public static final int output_filename = 0x21070004;
+ public static final int key_server_preference = 0x21070005;
+ public static final int look_up_key_id = 0x21070006;
+ public static final int export_to_server = 0x21070007;
+ public static final int import_from_qr_code = 0x21070008;
+ public static final int sign_key = 0x21070009;
+ }
+
+ public static final class dialog {
+ public static final int pass_phrase = 0x21070001;
+ public static final int encrypting = 0x21070002;
+ public static final int decrypting = 0x21070003;
+ public static final int new_pass_phrase = 0x21070004;
+ public static final int pass_phrases_do_not_match = 0x21070005;
+ public static final int no_pass_phrase = 0x21070006;
+ public static final int saving = 0x21070007;
+ public static final int delete_key = 0x21070008;
+ public static final int import_keys = 0x21070009;
+ public static final int importing = 0x2107000a;
+ public static final int export_key = 0x2107000b;
+ public static final int export_keys = 0x2107000c;
+ public static final int exporting = 0x2107000d;
+ public static final int new_account = 0x2107000e;
+// public static final int about = 0x2107000f;
+ public static final int change_log = 0x21070010;
+ public static final int output_filename = 0x21070011;
+ public static final int delete_file = 0x21070012;
+ public static final int deleting = 0x21070013;
+ public static final int help = 0x21070014;
+ public static final int querying = 0x21070015;
+ public static final int lookup_unknown_key = 0x21070016;
+ public static final int signing = 0x21070017;
+ }
+
+ public static final class task {
+ public static final int import_keys = 0x21070001;
+ public static final int export_keys = 0x21070002;
+ }
+
+ public static final class database {
+ public static final int type_public = 0;
+ public static final int type_secret = 1;
+ }
+
+ public static final class type {
+ public static final int public_key = 0x21070001;
+ public static final int secret_key = 0x21070002;
+ public static final int user_id = 0x21070003;
+ public static final int key = 0x21070004;
+ }
+
+ public static final class choice {
+ public static final class algorithm {
+ public static final int dsa = 0x21070001;
+ public static final int elgamal = 0x21070002;
+ public static final int rsa = 0x21070003;
+ }
+
+ public static final class compression {
+ public static final int none = 0x21070001;
+ public static final int zlib = CompressionAlgorithmTags.ZLIB;
+ public static final int bzip2 = CompressionAlgorithmTags.BZIP2;
+ public static final int zip = CompressionAlgorithmTags.ZIP;
+ }
+
+ public static final class usage {
+ public static final int sign_only = 0x21070001;
+ public static final int encrypt_only = 0x21070002;
+ public static final int sign_and_encrypt = 0x21070003;
+ }
+
+ public static final class action {
+ public static final int encrypt = 0x21070001;
+ public static final int decrypt = 0x21070002;
+ public static final int import_public = 0x21070003;
+ public static final int import_secret = 0x21070004;
+ }
+ }
+
+ public static final class return_value {
+ public static final int ok = 0;
+ public static final int error = -1;
+ public static final int no_master_key = -2;
+ public static final int updated = 1;
+ public static final int bad = -3;
+ }
+
+ public static final class target {
+ public static final int clipboard = 0x21070001;
+ public static final int email = 0x21070002;
+ public static final int file = 0x21070003;
+ public static final int message = 0x21070004;
+ }
+
+ public static final class mode {
+ public static final int undefined = 0x21070001;
+ public static final int byte_array = 0x21070002;
+ public static final int file = 0x21070003;
+ public static final int stream = 0x21070004;
+ }
+
+ public static final class key {
+ public static final int none = 0;
+ public static final int symmetric = -1;
+ }
+
+ public static final class content {
+ public static final int unknown = 0;
+ public static final int encrypted_data = 1;
+ public static final int keys = 2;
+ }
+
+ public static final class keyserver {
+ public static final int search = 0x21070001;
+ public static final int get = 0x21070002;
+ public static final int add = 0x21070003;
+ }
+}
diff --git a/org_apg/src/org/apg/InputData.java b/org_apg/src/org/apg/InputData.java
new file mode 100644
index 000000000..ae5b51fe4
--- /dev/null
+++ b/org_apg/src/org/apg/InputData.java
@@ -0,0 +1,25 @@
+package org.apg;
+
+import java.io.InputStream;
+
+public class InputData {
+ private PositionAwareInputStream mInputStream;
+ private long mSize;
+
+ public InputData(InputStream inputStream, long size) {
+ mInputStream = new PositionAwareInputStream(inputStream);
+ mSize = size;
+ }
+
+ public InputStream getInputStream() {
+ return mInputStream;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+
+ public long getStreamPosition() {
+ return mInputStream.position();
+ }
+}
diff --git a/org_apg/src/org/apg/KeyServer.java b/org_apg/src/org/apg/KeyServer.java
new file mode 100644
index 000000000..32158454f
--- /dev/null
+++ b/org_apg/src/org/apg/KeyServer.java
@@ -0,0 +1,46 @@
+package org.apg;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import java.util.Vector;
+
+public abstract class KeyServer {
+ static public class QueryException extends Exception {
+ private static final long serialVersionUID = 2703768928624654512L;
+
+ public QueryException(String message) {
+ super(message);
+ }
+ }
+
+ static public class TooManyResponses extends Exception {
+ private static final long serialVersionUID = 2703768928624654513L;
+ }
+
+ static public class InsufficientQuery extends Exception {
+ private static final long serialVersionUID = 2703768928624654514L;
+ }
+
+ static public class AddKeyException extends Exception {
+ private static final long serialVersionUID = -507574859137295530L;
+ }
+
+ static public class KeyInfo implements Serializable {
+ private static final long serialVersionUID = -7797972113284992662L;
+ public Vector<String> userIds;
+ public String revoked;
+ public Date date;
+ public String fingerPrint;
+ public long keyId;
+ public int size;
+ public String algorithm;
+ }
+
+ abstract List<KeyInfo> search(String query) throws QueryException, TooManyResponses,
+ InsufficientQuery;
+
+ abstract String get(long keyId) throws QueryException;
+
+ abstract void add(String armouredText) throws AddKeyException;
+}
diff --git a/org_apg/src/org/apg/PausableThread.java b/org_apg/src/org/apg/PausableThread.java
new file mode 100644
index 000000000..87258d36c
--- /dev/null
+++ b/org_apg/src/org/apg/PausableThread.java
@@ -0,0 +1,35 @@
+package org.apg;
+
+public class PausableThread extends Thread {
+ private boolean mPaused = false;
+
+ public PausableThread(Runnable runnable) {
+ super(runnable);
+ }
+
+ public void pause() {
+ synchronized (this) {
+ mPaused = true;
+ while (mPaused) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ public void unpause() {
+ synchronized (this) {
+ mPaused = false;
+ notify();
+ }
+ }
+
+ public boolean isPaused() {
+ synchronized (this) {
+ return mPaused;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/PositionAwareInputStream.java b/org_apg/src/org/apg/PositionAwareInputStream.java
new file mode 100644
index 000000000..c59459670
--- /dev/null
+++ b/org_apg/src/org/apg/PositionAwareInputStream.java
@@ -0,0 +1,67 @@
+package org.apg;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class PositionAwareInputStream extends InputStream {
+ private InputStream mStream;
+ private long mPosition;
+
+ public PositionAwareInputStream(InputStream in) {
+ mStream = in;
+ mPosition = 0;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int ch = mStream.read();
+ ++mPosition;
+ return ch;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mStream.close();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int result = mStream.read(b);
+ mPosition += result;
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ int result = mStream.read(b, offset, length);
+ mPosition += result;
+ return result;
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ mStream.reset();
+ mPosition = 0;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long result = mStream.skip(n);
+ mPosition += result;
+ return result;
+ }
+
+ public long position() {
+ return mPosition;
+ }
+}
diff --git a/org_apg/src/org/apg/Preferences.java b/org_apg/src/org/apg/Preferences.java
new file mode 100644
index 000000000..f4800e064
--- /dev/null
+++ b/org_apg/src/org/apg/Preferences.java
@@ -0,0 +1,170 @@
+package org.apg;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.Vector;
+
+public class Preferences {
+ private static Preferences mPreferences;
+ private SharedPreferences mSharedPreferences;
+
+ public static synchronized Preferences getPreferences(Context context) {
+ return getPreferences(context, false);
+ }
+
+ public static synchronized Preferences getPreferences(Context context, boolean force_new) {
+ if (mPreferences == null || force_new) {
+ mPreferences = new Preferences(context);
+ }
+ return mPreferences;
+ }
+
+ private Preferences(Context context) {
+ mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_PRIVATE);
+ }
+
+ public String getLanguage() {
+ return mSharedPreferences.getString(Constants.pref.LANGUAGE, "");
+ }
+
+ public void setLanguage(String value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Constants.pref.LANGUAGE, value);
+ editor.commit();
+ }
+
+ public int getPassPhraseCacheTtl() {
+ int ttl = mSharedPreferences.getInt(Constants.pref.PASS_PHRASE_CACHE_TTL, 180);
+ // fix the value if it was set to "never" in previous versions, which currently is not
+ // supported
+ if (ttl == 0) {
+ ttl = 180;
+ }
+ return ttl;
+ }
+
+ public void setPassPhraseCacheTtl(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.PASS_PHRASE_CACHE_TTL, value);
+ editor.commit();
+ }
+
+ public int getDefaultEncryptionAlgorithm() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM,
+ PGPEncryptedData.AES_256);
+ }
+
+ public void setDefaultEncryptionAlgorithm(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, value);
+ editor.commit();
+ }
+
+ public int getDefaultHashAlgorithm() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_HASH_ALGORITHM,
+ HashAlgorithmTags.SHA256);
+ }
+
+ public void setDefaultHashAlgorithm(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_HASH_ALGORITHM, value);
+ editor.commit();
+ }
+
+ public int getDefaultMessageCompression() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION,
+ Id.choice.compression.zlib);
+ }
+
+ public void setDefaultMessageCompression(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, value);
+ editor.commit();
+ }
+
+ public int getDefaultFileCompression() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_FILE_COMPRESSION,
+ Id.choice.compression.none);
+ }
+
+ public void setDefaultFileCompression(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_FILE_COMPRESSION, value);
+ editor.commit();
+ }
+
+ public boolean getDefaultAsciiArmour() {
+ return mSharedPreferences.getBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, false);
+ }
+
+ public void setDefaultAsciiArmour(boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, value);
+ editor.commit();
+ }
+
+ public boolean getForceV3Signatures() {
+ return mSharedPreferences.getBoolean(Constants.pref.FORCE_V3_SIGNATURES, false);
+ }
+
+ public void setForceV3Signatures(boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.FORCE_V3_SIGNATURES, value);
+ editor.commit();
+ }
+
+ public boolean hasSeenChangeLog(String version) {
+ return mSharedPreferences.getBoolean(Constants.pref.HAS_SEEN_CHANGE_LOG + version, false);
+ }
+
+ public void setHasSeenChangeLog(String version, boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.HAS_SEEN_CHANGE_LOG + version, value);
+ editor.commit();
+ }
+
+ public boolean hasSeenHelp() {
+ return mSharedPreferences.getBoolean(Constants.pref.HAS_SEEN_HELP, false);
+ }
+
+ public void setHasSeenHelp(boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.HAS_SEEN_HELP, value);
+ editor.commit();
+ }
+
+ public String[] getKeyServers() {
+ String rawData = mSharedPreferences.getString(Constants.pref.KEY_SERVERS,
+ Constants.defaults.KEY_SERVERS);
+ Vector<String> servers = new Vector<String>();
+ String chunks[] = rawData.split(",");
+ for (int i = 0; i < chunks.length; ++i) {
+ String tmp = chunks[i].trim();
+ if (tmp.length() > 0) {
+ servers.add(tmp);
+ }
+ }
+ return servers.toArray(chunks);
+ }
+
+ public void setKeyServers(String[] value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ String rawData = "";
+ for (int i = 0; i < value.length; ++i) {
+ String tmp = value[i].trim();
+ if (tmp.length() == 0) {
+ continue;
+ }
+ if (!"".equals(rawData)) {
+ rawData += ",";
+ }
+ rawData += tmp;
+ }
+ editor.putString(Constants.pref.KEY_SERVERS, rawData);
+ editor.commit();
+ }
+}
diff --git a/org_apg/src/org/apg/Primes.java b/org_apg/src/org/apg/Primes.java
new file mode 100644
index 000000000..1fd6482b2
--- /dev/null
+++ b/org_apg/src/org/apg/Primes.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+import java.math.BigInteger;
+
+public final class Primes {
+ // taken from http://www.ietf.org/rfc/rfc3526.txt
+ public static final String P1536 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF";
+
+ public static final String P2048 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF";
+
+ public static final String P3072 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF";
+
+ public static final String P4096 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" +
+ "FFFFFFFF FFFFFFFF";
+
+ public static final String P6144 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" +
+ "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" +
+ "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" +
+ "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" +
+ "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" +
+ "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" +
+ "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" +
+ "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" +
+ "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" +
+ "12BF2D5B 0B7474D6 E694F91E 6DCC4024 FFFFFFFF FFFFFFFF";
+
+ public static final String P8192 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" +
+ "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" +
+ "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" +
+ "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" +
+ "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" +
+ "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" +
+ "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" +
+ "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" +
+ "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" +
+ "12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4" +
+ "38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300" +
+ "741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568" +
+ "3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" +
+ "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B" +
+ "4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A" +
+ "062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36" +
+ "4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1" +
+ "B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92" +
+ "4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47" +
+ "9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" +
+ "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF";
+
+ public static BigInteger getBestPrime(int keySize) {
+ String primeString;
+ if (keySize >= (8192 + 6144) / 2) {
+ primeString = P8192;
+ } else if (keySize >= (6144 + 4096) / 2) {
+ primeString = P6144;
+ } else if (keySize >= (4096 + 3072) / 2) {
+ primeString = P4096;
+ } else if (keySize >= (3072 + 2048) / 2) {
+ primeString = P3072;
+ } else if (keySize >= (2048 + 1536) / 2) {
+ primeString = P2048;
+ } else {
+ primeString = P1536;
+ }
+
+ return new BigInteger(primeString.replaceAll(" ", ""), 16);
+ }
+}
diff --git a/org_apg/src/org/apg/ProgressDialogUpdater.java b/org_apg/src/org/apg/ProgressDialogUpdater.java
new file mode 100644
index 000000000..043cca906
--- /dev/null
+++ b/org_apg/src/org/apg/ProgressDialogUpdater.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg;
+
+public interface ProgressDialogUpdater {
+ void setProgress(String message, int current, int total);
+
+ void setProgress(int resourceId, int current, int total);
+
+ void setProgress(int current, int total);
+}
diff --git a/org_apg/src/org/apg/Service.java b/org_apg/src/org/apg/Service.java
new file mode 100644
index 000000000..24c4daa11
--- /dev/null
+++ b/org_apg/src/org/apg/Service.java
@@ -0,0 +1,78 @@
+package org.apg;
+
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+
+public class Service extends android.app.Service {
+ private final IBinder mBinder = new LocalBinder();
+
+ public static final String EXTRA_TTL = "ttl";
+
+ private int mPassPhraseCacheTtl = 15;
+ private Handler mCacheHandler = new Handler();
+ private Runnable mCacheTask = new Runnable() {
+ public void run() {
+ // check every ttl/2 seconds, which shouldn't be heavy on the device (even if ttl = 15),
+ // and makes sure the longest a pass phrase survives in the cache is 1.5 * ttl
+ int delay = mPassPhraseCacheTtl * 1000 / 2;
+ // also make sure the delay is not longer than one minute
+ if (delay > 60000) {
+ delay = 60000;
+ }
+
+ delay = Apg.cleanUpCache(mPassPhraseCacheTtl, delay);
+ // don't check too often, even if we were close
+ if (delay < 5000) {
+ delay = 5000;
+ }
+
+ mCacheHandler.postDelayed(this, delay);
+ }
+ };
+
+ static private boolean mIsRunning = false;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mIsRunning = true;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mIsRunning = false;
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ super.onStart(intent, startId);
+
+ if (intent != null) {
+ mPassPhraseCacheTtl = intent.getIntExtra(EXTRA_TTL, 15);
+ }
+ if (mPassPhraseCacheTtl < 15) {
+ mPassPhraseCacheTtl = 15;
+ }
+ mCacheHandler.removeCallbacks(mCacheTask);
+ mCacheHandler.postDelayed(mCacheTask, 1000);
+ }
+
+ static public boolean isRunning() {
+ return mIsRunning;
+ }
+
+ public class LocalBinder extends Binder {
+ Service getService() {
+ return Service.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/org_apg/src/org/apg/provider/Accounts.java b/org_apg/src/org/apg/provider/Accounts.java
new file mode 100644
index 000000000..b95f079df
--- /dev/null
+++ b/org_apg/src/org/apg/provider/Accounts.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.provider;
+
+import android.provider.BaseColumns;
+
+public class Accounts implements BaseColumns {
+ public static final String TABLE_NAME = "accounts";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String NAME = "c_name";
+ public static final String NAME_type = "TEXT";
+}
diff --git a/org_apg/src/org/apg/provider/ApgServiceBlobDatabase.java b/org_apg/src/org/apg/provider/ApgServiceBlobDatabase.java
new file mode 100644
index 000000000..70b9cd64f
--- /dev/null
+++ b/org_apg/src/org/apg/provider/ApgServiceBlobDatabase.java
@@ -0,0 +1,54 @@
+package org.apg.provider;
+
+import org.apg.ApgService;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+public class ApgServiceBlobDatabase extends SQLiteOpenHelper {
+
+ private static final String TAG = "ApgServiceBlobDatabase";
+
+ private static final int VERSION = 1;
+ private static final String NAME = "apg_service_blob_data";
+ private static final String TABLE = "data";
+
+ public ApgServiceBlobDatabase(Context context) {
+ super(context, NAME, null, VERSION);
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "constructor called");
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "onCreate() called");
+ db.execSQL("create table " + TABLE + " ( _id integer primary key autoincrement," +
+ "key text not null)");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "onUpgrade() called");
+ // no upgrade necessary yet
+ }
+
+ public Uri insert(ContentValues vals) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "insert() called");
+ SQLiteDatabase db = this.getWritableDatabase();
+ long newId = db.insert(TABLE, null, vals);
+ return ContentUris.withAppendedId(ApgServiceBlobProvider.CONTENT_URI, newId);
+ }
+
+ public Cursor query(String id, String key) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "query() called");
+ SQLiteDatabase db = this.getReadableDatabase();
+ return db.query(TABLE, new String[] {"_id"},
+ "_id = ? and key = ?", new String[] {id, key},
+ null, null, null);
+ }
+}
diff --git a/org_apg/src/org/apg/provider/ApgServiceBlobProvider.java b/org_apg/src/org/apg/provider/ApgServiceBlobProvider.java
new file mode 100644
index 000000000..f2d5377a4
--- /dev/null
+++ b/org_apg/src/org/apg/provider/ApgServiceBlobProvider.java
@@ -0,0 +1,138 @@
+package org.apg.provider;
+
+import org.apg.ApgService;
+import org.apg.Constants;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+public class ApgServiceBlobProvider extends ContentProvider {
+
+ private static final String TAG = "ApgServiceBlobProvider";
+
+ public static final Uri CONTENT_URI = Uri.parse("content://org.thialfihar.android.apg.provider.apgserviceblobprovider");
+
+ private static final String COLUMN_KEY = "key";
+
+ private static final String STORE_PATH = Constants.path.APP_DIR+"/ApgServiceBlobs";
+
+ private ApgServiceBlobDatabase mDb = null;
+
+ public ApgServiceBlobProvider() {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "Constructor called");
+ File dir = new File(STORE_PATH);
+ dir.mkdirs();
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "Constructor finished");
+ }
+
+ @Override
+ public int delete(Uri arg0, String arg1, String[] arg2) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "delete() called");
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri arg0) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "getType() called");
+ // not needed for now
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues ignored) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "insert() called");
+ // ContentValues are actually ignored, because we want to store a blob with no more information
+ // but have to create an record with the password generated here first
+
+ ContentValues vals = new ContentValues();
+
+ // Insert a random key in the database. This has to provided by the caller when updating or
+ // getting the blob
+ String password = UUID.randomUUID().toString();
+ vals.put(COLUMN_KEY, password);
+
+ Uri insertedUri = mDb.insert(vals);
+ return Uri.withAppendedPath(insertedUri, password);
+ }
+
+ @Override
+ public boolean onCreate() {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "onCreate() called");
+ mDb = new ApgServiceBlobDatabase(getContext());
+ // TODO Auto-generated method stub
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "query() called");
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "update() called");
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException, FileNotFoundException {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "openFile() called");
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... with uri: "+uri.toString());
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... with mode: "+mode);
+
+ List<String> segments = uri.getPathSegments();
+ if(segments.size() < 2) {
+ throw new SecurityException("Password not found in URI");
+ }
+ String id = segments.get(0);
+ String key = segments.get(1);
+
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... got id: "+id);
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... and key: "+key);
+
+ // get the data
+ Cursor result = mDb.query(id, key);
+
+ if(result.getCount() == 0) {
+ // either the key is wrong or no id exists
+ throw new FileNotFoundException("No file found with that ID and/or password");
+ }
+
+ File targetFile = new File(STORE_PATH, id);
+ if(mode.equals("w")) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... will try to open file w");
+ if( !targetFile.exists() ) {
+ try {
+ targetFile.createNewFile();
+ } catch (IOException e) {
+ Log.e(TAG, "... got IEOException on creating new file", e);
+ throw new FileNotFoundException("Could not create file to write to");
+ }
+ }
+ return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE );
+ } else if(mode.equals("r")) {
+ if(ApgService.LOCAL_LOGD) Log.d(TAG, "... will try to open file r");
+ if( !targetFile.exists() ) {
+ throw new FileNotFoundException("Error: Could not find the file requested");
+ }
+ return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+
+ return null;
+ }
+
+}
diff --git a/org_apg/src/org/apg/provider/DataProvider.java b/org_apg/src/org/apg/provider/DataProvider.java
new file mode 100644
index 000000000..a97851e58
--- /dev/null
+++ b/org_apg/src/org/apg/provider/DataProvider.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.provider;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.HashMap;
+
+import org.apg.Id;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+
+public class DataProvider extends ContentProvider {
+ public static final String AUTHORITY = "org.thialfihar.android.apg.provider";
+
+ private static final int PUBLIC_KEY_RING = 101;
+ private static final int PUBLIC_KEY_RING_ID = 102;
+ private static final int PUBLIC_KEY_RING_BY_KEY_ID = 103;
+ private static final int PUBLIC_KEY_RING_BY_EMAILS = 104;
+ private static final int PUBLIC_KEY_RING_KEY = 111;
+ private static final int PUBLIC_KEY_RING_KEY_RANK = 112;
+ private static final int PUBLIC_KEY_RING_USER_ID = 121;
+ private static final int PUBLIC_KEY_RING_USER_ID_RANK = 122;
+
+ private static final int SECRET_KEY_RING = 201;
+ private static final int SECRET_KEY_RING_ID = 202;
+ private static final int SECRET_KEY_RING_BY_KEY_ID = 203;
+ private static final int SECRET_KEY_RING_BY_EMAILS = 204;
+ private static final int SECRET_KEY_RING_KEY = 211;
+ private static final int SECRET_KEY_RING_KEY_RANK = 212;
+ private static final int SECRET_KEY_RING_USER_ID = 221;
+ private static final int SECRET_KEY_RING_USER_ID_RANK = 222;
+
+ private static final int DATA_STREAM = 301;
+
+ private static final String PUBLIC_KEY_RING_CONTENT_DIR_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.public.key_ring";
+ private static final String PUBLIC_KEY_RING_CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.public.key_ring";
+
+ private static final String PUBLIC_KEY_CONTENT_DIR_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.public.key";
+ private static final String PUBLIC_KEY_CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.public.key";
+
+ private static final String SECRET_KEY_RING_CONTENT_DIR_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.secret.key_ring";
+ private static final String SECRET_KEY_RING_CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.secret.key_ring";
+
+ private static final String SECRET_KEY_CONTENT_DIR_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.secret.key";
+ private static final String SECRET_KEY_CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.secret.key";
+
+ private static final String USER_ID_CONTENT_DIR_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.user_id";
+ private static final String USER_ID_CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.user_id";
+
+ public static final String _ID = "_id";
+ public static final String MASTER_KEY_ID = "master_key_id";
+ public static final String KEY_ID = "key_id";
+ public static final String USER_ID = "user_id";
+
+ private static final UriMatcher mUriMatcher;
+
+ private Database mDb;
+
+ static {
+ mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/key_id/*", PUBLIC_KEY_RING_BY_KEY_ID);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/emails/*", PUBLIC_KEY_RING_BY_EMAILS);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/*/keys", PUBLIC_KEY_RING_KEY);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/*/keys/#", PUBLIC_KEY_RING_KEY_RANK);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/*/user_ids", PUBLIC_KEY_RING_USER_ID);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/*/user_ids/#", PUBLIC_KEY_RING_USER_ID_RANK);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public", PUBLIC_KEY_RING);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/public/*", PUBLIC_KEY_RING_ID);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/key_id/*", SECRET_KEY_RING_BY_KEY_ID);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/emails/*", SECRET_KEY_RING_BY_EMAILS);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/*/keys", SECRET_KEY_RING_KEY);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/*/keys/#", SECRET_KEY_RING_KEY_RANK);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/*/user_ids", SECRET_KEY_RING_USER_ID);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/*/user_ids/#", SECRET_KEY_RING_USER_ID_RANK);
+
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret", SECRET_KEY_RING);
+ mUriMatcher.addURI(AUTHORITY, "key_rings/secret/*", SECRET_KEY_RING_ID);
+
+ mUriMatcher.addURI(AUTHORITY, "data/*", DATA_STREAM);
+ }
+
+ @Override
+ public boolean onCreate() {
+ mDb = new Database(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ // TODO: implement the others, then use them for the lists
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ HashMap<String, String> projectionMap = new HashMap<String, String>();
+
+ int match = mUriMatcher.match(uri);
+ int type;
+ switch (match) {
+ case PUBLIC_KEY_RING:
+ case PUBLIC_KEY_RING_ID:
+ case PUBLIC_KEY_RING_BY_KEY_ID:
+ case PUBLIC_KEY_RING_BY_EMAILS:
+ case PUBLIC_KEY_RING_KEY:
+ case PUBLIC_KEY_RING_KEY_RANK:
+ case PUBLIC_KEY_RING_USER_ID:
+ case PUBLIC_KEY_RING_USER_ID_RANK:
+ type = Id.database.type_public;
+ break;
+
+ case SECRET_KEY_RING:
+ case SECRET_KEY_RING_ID:
+ case SECRET_KEY_RING_BY_KEY_ID:
+ case SECRET_KEY_RING_BY_EMAILS:
+ case SECRET_KEY_RING_KEY:
+ case SECRET_KEY_RING_KEY_RANK:
+ case SECRET_KEY_RING_USER_ID:
+ case SECRET_KEY_RING_USER_ID_RANK:
+ type = Id.database.type_secret;
+ break;
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ qb.appendWhere(KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = " + type);
+
+ switch (match) {
+ case PUBLIC_KEY_RING_ID:
+ case SECRET_KEY_RING_ID: {
+ qb.appendWhere(" AND " +
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+
+ // break omitted intentionally
+ }
+
+ case PUBLIC_KEY_RING:
+ case SECRET_KEY_RING: {
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ projectionMap.put(_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings._ID);
+ projectionMap.put(MASTER_KEY_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID);
+ projectionMap.put(USER_ID,
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID);
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC";
+ }
+
+ break;
+ }
+
+ case SECRET_KEY_RING_BY_KEY_ID:
+ case PUBLIC_KEY_RING_BY_KEY_ID: {
+ qb.setTables(Keys.TABLE_NAME + " AS tmp INNER JOIN " +
+ KeyRings.TABLE_NAME + " ON (" +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ "tmp." + Keys.KEY_RING_ID + ")" +
+ " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ projectionMap.put(_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings._ID);
+ projectionMap.put(MASTER_KEY_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID);
+ projectionMap.put(USER_ID,
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID);
+
+ qb.appendWhere(" AND tmp." + Keys.KEY_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(3));
+
+ break;
+ }
+
+ case SECRET_KEY_RING_BY_EMAILS:
+ case PUBLIC_KEY_RING_BY_EMAILS: {
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ projectionMap.put(_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings._ID);
+ projectionMap.put(MASTER_KEY_ID,
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID);
+ projectionMap.put(USER_ID,
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID);
+
+ String emails = uri.getPathSegments().get(3);
+ String chunks[] = emails.split(" *, *");
+ boolean gotCondition = false;
+ String emailWhere = "";
+ for (int i = 0; i < chunks.length; ++i) {
+ if (chunks[i].length() == 0) {
+ continue;
+ }
+ if (i != 0) {
+ emailWhere += " OR ";
+ }
+ emailWhere += "tmp." + UserIds.USER_ID + " LIKE ";
+ // match '*<email>', so it has to be at the *end* of the user id
+ emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
+ gotCondition = true;
+ }
+
+ if (gotCondition) {
+ qb.appendWhere(" AND EXISTS (SELECT tmp." + UserIds._ID +
+ " FROM " + UserIds.TABLE_NAME +
+ " AS tmp WHERE tmp." + UserIds.KEY_ID + " = " +
+ Keys.TABLE_NAME + "." + Keys._ID +
+ " AND (" + emailWhere + "))");
+ }
+
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ qb.setProjectionMap(projectionMap);
+
+ // If no sort order is specified use the default
+ String orderBy;
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = null;
+ } else {
+ orderBy = sortOrder;
+ }
+
+ //System.out.println(qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null).replace("WHERE", "WHERE\n"));
+ Cursor c = qb.query(mDb.db(), projection, selection, selectionArgs, null, null, orderBy);
+
+ // Tell the cursor what uri to watch, so it knows when its source data changes
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEY_RING:
+ case PUBLIC_KEY_RING_BY_EMAILS:
+ return PUBLIC_KEY_RING_CONTENT_DIR_TYPE;
+
+ case PUBLIC_KEY_RING_ID:
+ return PUBLIC_KEY_RING_CONTENT_ITEM_TYPE;
+
+ case PUBLIC_KEY_RING_BY_KEY_ID:
+ return PUBLIC_KEY_RING_CONTENT_ITEM_TYPE;
+
+ case PUBLIC_KEY_RING_KEY:
+ return PUBLIC_KEY_CONTENT_DIR_TYPE;
+
+ case PUBLIC_KEY_RING_KEY_RANK:
+ return PUBLIC_KEY_CONTENT_ITEM_TYPE;
+
+ case PUBLIC_KEY_RING_USER_ID:
+ return USER_ID_CONTENT_DIR_TYPE;
+
+ case PUBLIC_KEY_RING_USER_ID_RANK:
+ return USER_ID_CONTENT_ITEM_TYPE;
+
+ case SECRET_KEY_RING:
+ case SECRET_KEY_RING_BY_EMAILS:
+ return SECRET_KEY_RING_CONTENT_DIR_TYPE;
+
+ case SECRET_KEY_RING_ID:
+ return SECRET_KEY_RING_CONTENT_ITEM_TYPE;
+
+ case SECRET_KEY_RING_BY_KEY_ID:
+ return SECRET_KEY_RING_CONTENT_ITEM_TYPE;
+
+ case SECRET_KEY_RING_KEY:
+ return SECRET_KEY_CONTENT_DIR_TYPE;
+
+ case SECRET_KEY_RING_KEY_RANK:
+ return SECRET_KEY_CONTENT_ITEM_TYPE;
+
+ case SECRET_KEY_RING_USER_ID:
+ return USER_ID_CONTENT_DIR_TYPE;
+
+ case SECRET_KEY_RING_USER_ID_RANK:
+ return USER_ID_CONTENT_ITEM_TYPE;
+
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues initialValues) {
+ // not supported
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String where, String[] whereArgs) {
+ // not supported
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+ // not supported
+ return 0;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ int match = mUriMatcher.match(uri);
+ if (match != DATA_STREAM) {
+ throw new FileNotFoundException();
+ }
+ String fileName = uri.getPathSegments().get(1);
+ File file = new File(getContext().getFilesDir().getAbsolutePath(), fileName);
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+}
diff --git a/org_apg/src/org/apg/provider/Database.java b/org_apg/src/org/apg/provider/Database.java
new file mode 100644
index 000000000..8b1aaa9e7
--- /dev/null
+++ b/org_apg/src/org/apg/provider/Database.java
@@ -0,0 +1,605 @@
+package org.apg.provider;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.util.IterableIterator;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Vector;
+
+public class Database extends SQLiteOpenHelper {
+ public static class GeneralException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public GeneralException(String message) {
+ super(message);
+ }
+ }
+
+ private static final String DATABASE_NAME = "apg";
+ private static final int DATABASE_VERSION = 2;
+
+ public static final String AUTHORITY = "org.thialfihar.android.apg.database";
+
+ public static HashMap<String, String> sKeyRingsProjection;
+ public static HashMap<String, String> sKeysProjection;
+ public static HashMap<String, String> sUserIdsProjection;
+
+ private SQLiteDatabase mDb = null;
+ private int mStatus = 0;
+
+ static {
+ sKeyRingsProjection = new HashMap<String, String>();
+ sKeyRingsProjection.put(KeyRings._ID, KeyRings._ID);
+ sKeyRingsProjection.put(KeyRings.MASTER_KEY_ID, KeyRings.MASTER_KEY_ID);
+ sKeyRingsProjection.put(KeyRings.TYPE, KeyRings.TYPE);
+ sKeyRingsProjection.put(KeyRings.WHO_ID, KeyRings.WHO_ID);
+ sKeyRingsProjection.put(KeyRings.KEY_RING_DATA, KeyRings.KEY_RING_DATA);
+
+ sKeysProjection = new HashMap<String, String>();
+ sKeysProjection.put(Keys._ID, Keys._ID);
+ sKeysProjection.put(Keys.KEY_ID, Keys.KEY_ID);
+ sKeysProjection.put(Keys.TYPE, Keys.TYPE);
+ sKeysProjection.put(Keys.IS_MASTER_KEY, Keys.IS_MASTER_KEY);
+ sKeysProjection.put(Keys.ALGORITHM, Keys.ALGORITHM);
+ sKeysProjection.put(Keys.KEY_SIZE, Keys.KEY_SIZE);
+ sKeysProjection.put(Keys.CAN_SIGN, Keys.CAN_SIGN);
+ sKeysProjection.put(Keys.CAN_ENCRYPT, Keys.CAN_ENCRYPT);
+ sKeysProjection.put(Keys.IS_REVOKED, Keys.IS_REVOKED);
+ sKeysProjection.put(Keys.CREATION, Keys.CREATION);
+ sKeysProjection.put(Keys.EXPIRY, Keys.EXPIRY);
+ sKeysProjection.put(Keys.KEY_DATA, Keys.KEY_DATA);
+ sKeysProjection.put(Keys.RANK, Keys.RANK);
+
+ sUserIdsProjection = new HashMap<String, String>();
+ sUserIdsProjection.put(UserIds._ID, UserIds._ID);
+ sUserIdsProjection.put(UserIds.KEY_ID, UserIds.KEY_ID);
+ sUserIdsProjection.put(UserIds.USER_ID, UserIds.USER_ID);
+ sUserIdsProjection.put(UserIds.RANK, UserIds.RANK);
+ }
+
+ public Database(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ // force upgrade to test things
+ //onUpgrade(getWritableDatabase(), 1, 2);
+ mDb = getWritableDatabase();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ mDb.close();
+ super.finalize();
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + KeyRings.TABLE_NAME + " (" +
+ KeyRings._ID + " " + KeyRings._ID_type + "," +
+ KeyRings.MASTER_KEY_ID + " " + KeyRings.MASTER_KEY_ID_type + ", " +
+ KeyRings.TYPE + " " + KeyRings.TYPE_type + ", " +
+ KeyRings.WHO_ID + " " + KeyRings.WHO_ID_type + ", " +
+ KeyRings.KEY_RING_DATA + " " + KeyRings.KEY_RING_DATA_type + ");");
+
+ db.execSQL("CREATE TABLE " + Keys.TABLE_NAME + " (" +
+ Keys._ID + " " + Keys._ID_type + "," +
+ Keys.KEY_ID + " " + Keys.KEY_ID_type + ", " +
+ Keys.TYPE + " " + Keys.TYPE_type + ", " +
+ Keys.IS_MASTER_KEY + " " + Keys.IS_MASTER_KEY_type + ", " +
+ Keys.ALGORITHM + " " + Keys.ALGORITHM_type + ", " +
+ Keys.KEY_SIZE + " " + Keys.KEY_SIZE_type + ", " +
+ Keys.CAN_SIGN + " " + Keys.CAN_SIGN_type + ", " +
+ Keys.CAN_ENCRYPT + " " + Keys.CAN_ENCRYPT_type + ", " +
+ Keys.IS_REVOKED + " " + Keys.IS_REVOKED_type + ", " +
+ Keys.CREATION + " " + Keys.CREATION_type + ", " +
+ Keys.EXPIRY + " " + Keys.EXPIRY_type + ", " +
+ Keys.KEY_RING_ID + " " + Keys.KEY_RING_ID_type + ", " +
+ Keys.KEY_DATA + " " + Keys.KEY_DATA_type +
+ Keys.RANK + " " + Keys.RANK_type + ");");
+
+ db.execSQL("CREATE TABLE " + UserIds.TABLE_NAME + " (" +
+ UserIds._ID + " " + UserIds._ID_type + "," +
+ UserIds.KEY_ID + " " + UserIds.KEY_ID_type + "," +
+ UserIds.USER_ID + " " + UserIds.USER_ID_type + "," +
+ UserIds.RANK + " " + UserIds.RANK_type + ");");
+
+ db.execSQL("CREATE TABLE " + Accounts.TABLE_NAME + " (" +
+ Accounts._ID + " " + Accounts._ID_type + "," +
+ Accounts.NAME + " " + Accounts.NAME_type + ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ mDb = db;
+ for (int version = oldVersion; version < newVersion; ++version) {
+ switch (version) {
+ case 1: { // upgrade 1 to 2
+ db.execSQL("DROP TABLE IF EXISTS " + KeyRings.TABLE_NAME + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Keys.TABLE_NAME + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + UserIds.TABLE_NAME + ";");
+
+ db.execSQL("CREATE TABLE " + KeyRings.TABLE_NAME + " (" +
+ KeyRings._ID + " " + KeyRings._ID_type + "," +
+ KeyRings.MASTER_KEY_ID + " " + KeyRings.MASTER_KEY_ID_type + ", " +
+ KeyRings.TYPE + " " + KeyRings.TYPE_type + ", " +
+ KeyRings.WHO_ID + " " + KeyRings.WHO_ID_type + ", " +
+ KeyRings.KEY_RING_DATA + " " + KeyRings.KEY_RING_DATA_type + ");");
+
+ db.execSQL("CREATE TABLE " + Keys.TABLE_NAME + " (" +
+ Keys._ID + " " + Keys._ID_type + "," +
+ Keys.KEY_ID + " " + Keys.KEY_ID_type + ", " +
+ Keys.TYPE + " " + Keys.TYPE_type + ", " +
+ Keys.IS_MASTER_KEY + " " + Keys.IS_MASTER_KEY_type + ", " +
+ Keys.ALGORITHM + " " + Keys.ALGORITHM_type + ", " +
+ Keys.KEY_SIZE + " " + Keys.KEY_SIZE_type + ", " +
+ Keys.CAN_SIGN + " " + Keys.CAN_SIGN_type + ", " +
+ Keys.CAN_ENCRYPT + " " + Keys.CAN_ENCRYPT_type + ", " +
+ Keys.IS_REVOKED + " " + Keys.IS_REVOKED_type + ", " +
+ Keys.CREATION + " " + Keys.CREATION_type + ", " +
+ Keys.EXPIRY + " " + Keys.EXPIRY_type + ", " +
+ Keys.KEY_RING_ID + " " + Keys.KEY_RING_ID_type + ", " +
+ Keys.KEY_DATA + " " + Keys.KEY_DATA_type +
+ Keys.RANK + " " + Keys.RANK_type + ");");
+
+ db.execSQL("CREATE TABLE " + UserIds.TABLE_NAME + " (" +
+ UserIds._ID + " " + UserIds._ID_type + "," +
+ UserIds.KEY_ID + " " + UserIds.KEY_ID_type + "," +
+ UserIds.USER_ID + " " + UserIds.USER_ID_type + "," +
+ UserIds.RANK + " " + UserIds.RANK_type + ");");
+
+ Cursor cursor = db.query("public_keys", new String[] { "c_key_data" },
+ null, null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ byte[] data = cursor.getBlob(0);
+ try {
+ PGPPublicKeyRing keyRing = new PGPPublicKeyRing(data);
+ saveKeyRing(keyRing);
+ } catch (IOException e) {
+ Log.e("apg.db.upgrade", "key import failed: " + e);
+ } catch (GeneralException e) {
+ Log.e("apg.db.upgrade", "key import failed: " + e);
+ }
+ } while (cursor.moveToNext());
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ cursor = db.query("secret_keys", new String[]{ "c_key_data" },
+ null, null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ byte[] data = cursor.getBlob(0);
+ try {
+ PGPSecretKeyRing keyRing = new PGPSecretKeyRing(data);
+ saveKeyRing(keyRing);
+ } catch (IOException e) {
+ Log.e("apg.db.upgrade", "key import failed: " + e);
+ } catch (PGPException e) {
+ Log.e("apg.db.upgrade", "key import failed: " + e);
+ } catch (GeneralException e) {
+ Log.e("apg.db.upgrade", "key import failed: " + e);
+ }
+ } while (cursor.moveToNext());
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ db.execSQL("DROP TABLE IF EXISTS public_keys;");
+ db.execSQL("DROP TABLE IF EXISTS secret_keys;");
+
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ mDb = null;
+ }
+
+ public int saveKeyRing(PGPPublicKeyRing keyRing) throws IOException, GeneralException {
+ mDb.beginTransaction();
+ ContentValues values = new ContentValues();
+ PGPPublicKey masterKey = keyRing.getPublicKey();
+ long masterKeyId = masterKey.getKeyID();
+
+ values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
+ values.put(KeyRings.TYPE, Id.database.type_public);
+ values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
+
+ long rowId = insertOrUpdateKeyRing(values);
+ int returnValue = mStatus;
+
+ if (rowId == -1) {
+ throw new GeneralException("saving public key ring " + masterKeyId + " failed");
+ }
+
+ Vector<Integer> seenIds = new Vector<Integer>();
+ int rank = 0;
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ seenIds.add(saveKey(rowId, key, rank));
+ ++rank;
+ }
+
+ String seenIdsStr = "";
+ for (Integer id : seenIds) {
+ if (seenIdsStr.length() > 0) {
+ seenIdsStr += ",";
+ }
+ seenIdsStr += id;
+ }
+ mDb.delete(Keys.TABLE_NAME,
+ Keys.KEY_RING_ID + " = ? AND " +
+ Keys._ID + " NOT IN (" + seenIdsStr + ")",
+ new String[] { "" + rowId });
+
+ mDb.setTransactionSuccessful();
+ mDb.endTransaction();
+ return returnValue;
+ }
+
+ public int saveKeyRing(PGPSecretKeyRing keyRing) throws IOException, GeneralException {
+ mDb.beginTransaction();
+ ContentValues values = new ContentValues();
+ PGPSecretKey masterKey = keyRing.getSecretKey();
+ long masterKeyId = masterKey.getKeyID();
+
+ values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
+ values.put(KeyRings.TYPE, Id.database.type_secret);
+ values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
+
+ long rowId = insertOrUpdateKeyRing(values);
+ int returnValue = mStatus;
+
+ if (rowId == -1) {
+ throw new GeneralException("saving secret key ring " + masterKeyId + " failed");
+ }
+
+ Vector<Integer> seenIds = new Vector<Integer>();
+ int rank = 0;
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ seenIds.add(saveKey(rowId, key, rank));
+ ++rank;
+ }
+
+ String seenIdsStr = "";
+ for (Integer id : seenIds) {
+ if (seenIdsStr.length() > 0) {
+ seenIdsStr += ",";
+ }
+ seenIdsStr += id;
+ }
+ mDb.delete(Keys.TABLE_NAME,
+ Keys.KEY_RING_ID + " = ? AND " +
+ Keys._ID + " NOT IN (" + seenIdsStr + ")",
+ new String[] { "" + rowId });
+
+ mDb.setTransactionSuccessful();
+ mDb.endTransaction();
+ return returnValue;
+ }
+
+ private int saveKey(long keyRingId, PGPPublicKey key, int rank)
+ throws IOException, GeneralException {
+ ContentValues values = new ContentValues();
+
+ values.put(Keys.KEY_ID, key.getKeyID());
+ values.put(Keys.TYPE, Id.database.type_public);
+ values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
+ values.put(Keys.ALGORITHM, key.getAlgorithm());
+ values.put(Keys.KEY_SIZE, key.getBitStrength());
+ values.put(Keys.CAN_SIGN, Apg.isSigningKey(key));
+ values.put(Keys.CAN_ENCRYPT, Apg.isEncryptionKey(key));
+ values.put(Keys.IS_REVOKED, key.isRevoked());
+ values.put(Keys.CREATION, Apg.getCreationDate(key).getTime() / 1000);
+ Date expiryDate = Apg.getExpiryDate(key);
+ if (expiryDate != null) {
+ values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
+ }
+ values.put(Keys.KEY_RING_ID, keyRingId);
+ values.put(Keys.KEY_DATA, key.getEncoded());
+ values.put(Keys.RANK, rank);
+
+ long rowId = insertOrUpdateKey(values);
+
+ if (rowId == -1) {
+ throw new GeneralException("saving public key " + key.getKeyID() + " failed");
+ }
+
+ Vector<Integer> seenIds = new Vector<Integer>();
+ int userIdRank = 0;
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ seenIds.add(saveUserId(rowId, userId, userIdRank));
+ ++userIdRank;
+ }
+
+ String seenIdsStr = "";
+ for (Integer id : seenIds) {
+ if (seenIdsStr.length() > 0) {
+ seenIdsStr += ",";
+ }
+ seenIdsStr += id;
+ }
+ mDb.delete(UserIds.TABLE_NAME,
+ UserIds.KEY_ID + " = ? AND " +
+ UserIds._ID + " NOT IN (" + seenIdsStr + ")",
+ new String[] { "" + rowId });
+
+ return (int)rowId;
+ }
+
+ private int saveKey(long keyRingId, PGPSecretKey key, int rank)
+ throws IOException, GeneralException {
+ ContentValues values = new ContentValues();
+
+ values.put(Keys.KEY_ID, key.getPublicKey().getKeyID());
+ values.put(Keys.TYPE, Id.database.type_secret);
+ values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
+ values.put(Keys.ALGORITHM, key.getPublicKey().getAlgorithm());
+ values.put(Keys.KEY_SIZE, key.getPublicKey().getBitStrength());
+ values.put(Keys.CAN_SIGN, Apg.isSigningKey(key));
+ values.put(Keys.CAN_ENCRYPT, Apg.isEncryptionKey(key));
+ values.put(Keys.IS_REVOKED, key.getPublicKey().isRevoked());
+ values.put(Keys.CREATION, Apg.getCreationDate(key).getTime() / 1000);
+ Date expiryDate = Apg.getExpiryDate(key);
+ if (expiryDate != null) {
+ values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
+ }
+ values.put(Keys.KEY_RING_ID, keyRingId);
+ values.put(Keys.KEY_DATA, key.getEncoded());
+ values.put(Keys.RANK, rank);
+
+ long rowId = insertOrUpdateKey(values);
+
+ if (rowId == -1) {
+ throw new GeneralException("saving secret key " + key.getPublicKey().getKeyID() + " failed");
+ }
+
+ Vector<Integer> seenIds = new Vector<Integer>();
+ int userIdRank = 0;
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ seenIds.add(saveUserId(rowId, userId, userIdRank));
+ ++userIdRank;
+ }
+
+ String seenIdsStr = "";
+ for (Integer id : seenIds) {
+ if (seenIdsStr.length() > 0) {
+ seenIdsStr += ",";
+ }
+ seenIdsStr += id;
+ }
+ mDb.delete(UserIds.TABLE_NAME,
+ UserIds.KEY_ID + " = ? AND " +
+ UserIds._ID + " NOT IN (" + seenIdsStr + ")",
+ new String[] { "" + rowId });
+
+ return (int)rowId;
+ }
+
+ private int saveUserId(long keyId, String userId, int rank) throws GeneralException {
+ ContentValues values = new ContentValues();
+
+ values.put(UserIds.KEY_ID, keyId);
+ values.put(UserIds.USER_ID, userId);
+ values.put(UserIds.RANK, rank);
+
+ long rowId = insertOrUpdateUserId(values);
+
+ if (rowId == -1) {
+ throw new GeneralException("saving user id " + userId + " failed");
+ }
+
+ return (int)rowId;
+ }
+
+ private long insertOrUpdateKeyRing(ContentValues values) {
+ Cursor c = mDb.query(KeyRings.TABLE_NAME, new String[] { KeyRings._ID },
+ KeyRings.MASTER_KEY_ID + " = ? AND " + KeyRings.TYPE + " = ?",
+ new String[] {
+ values.getAsString(KeyRings.MASTER_KEY_ID),
+ values.getAsString(KeyRings.TYPE),
+ },
+ null, null, null);
+ long rowId = -1;
+ if (c != null && c.moveToFirst()) {
+ rowId = c.getLong(0);
+ mDb.update(KeyRings.TABLE_NAME, values,
+ KeyRings._ID + " = ?", new String[] { "" + rowId });
+ mStatus = Id.return_value.updated;
+ } else {
+ rowId = mDb.insert(KeyRings.TABLE_NAME, KeyRings.WHO_ID, values);
+ mStatus = Id.return_value.ok;
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return rowId;
+ }
+
+ private long insertOrUpdateKey(ContentValues values) {
+ Cursor c = mDb.query(Keys.TABLE_NAME, new String[] { Keys._ID },
+ Keys.KEY_ID + " = ? AND " + Keys.TYPE + " = ?",
+ new String[] {
+ values.getAsString(Keys.KEY_ID),
+ values.getAsString(Keys.TYPE),
+ },
+ null, null, null);
+ long rowId = -1;
+ if (c != null && c.moveToFirst()) {
+ rowId = c.getLong(0);
+ mDb.update(Keys.TABLE_NAME, values,
+ Keys._ID + " = ?", new String[] { "" + rowId });
+ } else {
+ rowId = mDb.insert(Keys.TABLE_NAME, Keys.KEY_DATA, values);
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return rowId;
+ }
+
+ private long insertOrUpdateUserId(ContentValues values) {
+ Cursor c = mDb.query(UserIds.TABLE_NAME, new String[] { UserIds._ID },
+ UserIds.KEY_ID + " = ? AND " + UserIds.USER_ID + " = ?",
+ new String[] {
+ values.getAsString(UserIds.KEY_ID),
+ values.getAsString(UserIds.USER_ID),
+ },
+ null, null, null);
+ long rowId = -1;
+ if (c != null && c.moveToFirst()) {
+ rowId = c.getLong(0);
+ mDb.update(UserIds.TABLE_NAME, values,
+ UserIds._ID + " = ?", new String[] { "" + rowId });
+ } else {
+ rowId = mDb.insert(UserIds.TABLE_NAME, UserIds.USER_ID, values);
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return rowId;
+ }
+
+ public Object getKeyRing(int keyRingId) {
+ Cursor c = mDb.query(KeyRings.TABLE_NAME,
+ new String[] { KeyRings.KEY_RING_DATA, KeyRings.TYPE },
+ KeyRings._ID + " = ?",
+ new String[] {
+ "" + keyRingId,
+ },
+ null, null, null);
+ byte[] data = null;
+ Object keyRing = null;
+ if (c != null && c.moveToFirst()) {
+ data = c.getBlob(0);
+ if (data != null) {
+ try {
+ if (c.getInt(1) == Id.database.type_public) {
+ keyRing = new PGPPublicKeyRing(data);
+ } else {
+ keyRing = new PGPSecretKeyRing(data);
+ }
+ } catch (IOException e) {
+ // can't load it, then
+ } catch (PGPException e) {
+ // can't load it, then
+ }
+ }
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return keyRing;
+ }
+
+ public byte[] getKeyRingDataFromKeyId(int type, long keyId) {
+ Cursor c = mDb.query(Keys.TABLE_NAME + " INNER JOIN " + KeyRings.TABLE_NAME + " ON (" +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + ")",
+ new String[] { KeyRings.TABLE_NAME + "." + KeyRings.KEY_RING_DATA },
+ Keys.TABLE_NAME + "." + Keys.KEY_ID + " = ? AND " +
+ KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?",
+ new String[] {
+ "" + keyId,
+ "" + type,
+ },
+ null, null, null);
+
+ byte[] data = null;
+ if (c != null && c.moveToFirst()) {
+ data = c.getBlob(0);
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return data;
+ }
+
+ public byte[] getKeyDataFromKeyId(int type, long keyId) {
+ Cursor c = mDb.query(Keys.TABLE_NAME, new String[] { Keys.KEY_DATA },
+ Keys.KEY_ID + " = ? AND " + Keys.TYPE + " = ?",
+ new String[] {
+ "" + keyId,
+ "" + type,
+ },
+ null, null, null);
+ byte[] data = null;
+ if (c != null && c.moveToFirst()) {
+ data = c.getBlob(0);
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ return data;
+ }
+
+ public void deleteKeyRing(int keyRingId) {
+ mDb.beginTransaction();
+ mDb.delete(KeyRings.TABLE_NAME,
+ KeyRings._ID + " = ?", new String[] { "" + keyRingId });
+
+ Cursor c = mDb.query(Keys.TABLE_NAME, new String[] { Keys._ID },
+ Keys.KEY_RING_ID + " = ?",
+ new String[] {
+ "" + keyRingId,
+ },
+ null, null, null);
+ if (c != null && c.moveToFirst()) {
+ do {
+ int keyId = c.getInt(0);
+ deleteKey(keyId);
+ } while (c.moveToNext());
+ }
+
+ if (c != null) {
+ c.close();
+ }
+
+ mDb.setTransactionSuccessful();
+ mDb.endTransaction();
+ }
+
+ private void deleteKey(int keyId) {
+ mDb.delete(Keys.TABLE_NAME,
+ Keys._ID + " = ?", new String[] { "" + keyId });
+
+ mDb.delete(UserIds.TABLE_NAME,
+ UserIds.KEY_ID + " = ?", new String[] { "" + keyId });
+ }
+
+ public SQLiteDatabase db() {
+ return mDb;
+ }
+}
diff --git a/org_apg/src/org/apg/provider/KeyRings.java b/org_apg/src/org/apg/provider/KeyRings.java
new file mode 100644
index 000000000..304afd24f
--- /dev/null
+++ b/org_apg/src/org/apg/provider/KeyRings.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.provider;
+
+import android.provider.BaseColumns;
+
+public class KeyRings implements BaseColumns {
+ public static final String TABLE_NAME = "key_rings";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String MASTER_KEY_ID = "c_master_key_id";
+ public static final String MASTER_KEY_ID_type = "INT64";
+ public static final String TYPE = "c_type";
+ public static final String TYPE_type = "INTEGER";
+ public static final String WHO_ID = "c_who_id";
+ public static final String WHO_ID_type = "INTEGER";
+ public static final String KEY_RING_DATA = "c_key_ring_data";
+ public static final String KEY_RING_DATA_type = "BLOB";
+}
diff --git a/org_apg/src/org/apg/provider/Keys.java b/org_apg/src/org/apg/provider/Keys.java
new file mode 100644
index 000000000..63eaee54f
--- /dev/null
+++ b/org_apg/src/org/apg/provider/Keys.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.provider;
+
+import android.provider.BaseColumns;
+
+public class Keys implements BaseColumns {
+ public static final String TABLE_NAME = "keys";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String KEY_ID = "c_key_id";
+ public static final String KEY_ID_type = "INT64";
+ public static final String TYPE = "c_type";
+ public static final String TYPE_type = "INTEGER";
+ public static final String IS_MASTER_KEY = "c_is_master_key";
+ public static final String IS_MASTER_KEY_type = "INTEGER";
+ public static final String ALGORITHM = "c_algorithm";
+ public static final String ALGORITHM_type = "INTEGER";
+ public static final String KEY_SIZE = "c_key_size";
+ public static final String KEY_SIZE_type = "INTEGER";
+ public static final String CAN_SIGN = "c_can_sign";
+ public static final String CAN_SIGN_type = "INTEGER";
+ public static final String CAN_ENCRYPT = "c_can_encrypt";
+ public static final String CAN_ENCRYPT_type = "INTEGER";
+ public static final String IS_REVOKED = "c_is_revoked";
+ public static final String IS_REVOKED_type = "INTEGER";
+ public static final String CREATION = "c_creation";
+ public static final String CREATION_type = "INTEGER";
+ public static final String EXPIRY = "c_expiry";
+ public static final String EXPIRY_type = "INTEGER";
+ public static final String KEY_RING_ID = "c_key_ring_id";
+ public static final String KEY_RING_ID_type = "INTEGER";
+ public static final String KEY_DATA = "c_key_data";
+ public static final String KEY_DATA_type = "BLOB";
+ public static final String RANK = "c_key_data";
+ public static final String RANK_type = "INTEGER";
+}
diff --git a/org_apg/src/org/apg/provider/UserIds.java b/org_apg/src/org/apg/provider/UserIds.java
new file mode 100644
index 000000000..e8ddc677d
--- /dev/null
+++ b/org_apg/src/org/apg/provider/UserIds.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.provider;
+
+import android.provider.BaseColumns;
+
+public class UserIds implements BaseColumns {
+ public static final String TABLE_NAME = "user_ids";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String KEY_ID = "c_key_id";
+ public static final String KEY_ID_type = "INTEGER";
+ public static final String USER_ID = "c_user_id";
+ public static final String USER_ID_type = "TEXT";
+ public static final String RANK = "c_rank";
+ public static final String RANK_type = "INTEGER";
+}
diff --git a/org_apg/src/org/apg/ui/AboutActivity.java b/org_apg/src/org/apg/ui/AboutActivity.java
new file mode 100644
index 000000000..308a1e06e
--- /dev/null
+++ b/org_apg/src/org/apg/ui/AboutActivity.java
@@ -0,0 +1,51 @@
+package org.apg.ui;
+
+import org.apg.Constants;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+public class AboutActivity extends Activity {
+ Activity mActivity;
+
+ /**
+ * Instantiate View for this Activity
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.about_activity);
+
+ mActivity = this;
+
+ TextView versionText = (TextView) findViewById(R.id.about_version);
+ versionText.setText(getString(R.string.about_version) + " " + getVersion());
+ }
+
+ /**
+ * Get the current package version.
+ *
+ * @return The current version.
+ */
+ private String getVersion() {
+ String result = "";
+ try {
+ PackageManager manager = mActivity.getPackageManager();
+ PackageInfo info = manager.getPackageInfo(mActivity.getPackageName(), 0);
+
+ result = String.format("%s (%s)", info.versionName, info.versionCode);
+ } catch (NameNotFoundException e) {
+ Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage());
+ result = "Unable to get application version.";
+ }
+
+ return result;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/BaseActivity.java b/org_apg/src/org/apg/ui/BaseActivity.java
new file mode 100644
index 000000000..9b5039a5d
--- /dev/null
+++ b/org_apg/src/org/apg/ui/BaseActivity.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Locale;
+
+import org.apg.Apg;
+import org.apg.AskForSecretKeyPassPhrase;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.PausableThread;
+import org.apg.Preferences;
+import org.apg.ProgressDialogUpdater;
+import org.apg.Service;
+import org.apg.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class BaseActivity extends Activity implements Runnable, ProgressDialogUpdater,
+ AskForSecretKeyPassPhrase.PassPhraseCallbackInterface {
+
+ private ProgressDialog mProgressDialog = null;
+ private PausableThread mRunningThread = null;
+ private Thread mDeletingThread = null;
+
+ private long mSecretKeyId = 0;
+ private String mDeleteFile = null;
+
+ protected Preferences mPreferences;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ handlerCallback(msg);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPreferences = Preferences.getPreferences(this);
+ setLanguage(this, mPreferences.getLanguage());
+
+ Apg.initialize(this);
+
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ File dir = new File(Constants.path.APP_DIR);
+ if (!dir.exists() && !dir.mkdirs()) {
+ // ignore this for now, it's not crucial
+ // that the directory doesn't exist at this point
+ }
+ }
+
+ startCacheService(this, mPreferences);
+ }
+
+ public static void startCacheService(Activity activity, Preferences preferences) {
+ Intent intent = new Intent(activity, Service.class);
+ intent.putExtra(Service.EXTRA_TTL, preferences.getPassPhraseCacheTtl());
+ activity.startService(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(0, Id.menu.option.about, 1, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.about: {
+ startActivity(new Intent(this, AboutActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.preferences: {
+ startActivity(new Intent(this, PreferencesActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.search: {
+ startSearch("", false, null, false);
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ // in case it is a progress dialog
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ switch (id) {
+ case Id.dialog.encrypting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.decrypting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.saving: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_saving));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.importing: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_importing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.exporting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_exporting));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.deleting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.querying: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_querying));
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case Id.dialog.signing: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_signing));
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ default: {
+ break;
+ }
+ }
+ mProgressDialog = null;
+
+ switch (id) {
+
+ case Id.dialog.pass_phrase: {
+ return AskForSecretKeyPassPhrase.createDialog(this, getSecretKeyId(), this);
+ }
+
+ case Id.dialog.pass_phrases_do_not_match: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.error);
+ alert.setMessage(R.string.passPhrasesDoNotMatch);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.pass_phrases_do_not_match);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ case Id.dialog.no_pass_phrase: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.error);
+ alert.setMessage(R.string.passPhraseMustNotBeEmpty);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.no_pass_phrase);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ case Id.dialog.delete_file: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.fileDeleteConfirmation, getDeleteFile()));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_file);
+ final File file = new File(getDeleteFile());
+ showDialog(Id.dialog.deleting);
+ mDeletingThread = new Thread(new Runnable() {
+ public void run() {
+ Bundle data = new Bundle();
+ data.putInt(Constants.extras.STATUS, Id.message.delete_done);
+ try {
+ Apg.deleteFileSecurely(BaseActivity.this, file, BaseActivity.this);
+ } catch (FileNotFoundException e) {
+ data.putString(Apg.EXTRA_ERROR, BaseActivity.this.getString(
+ R.string.error_fileNotFound, file));
+ } catch (IOException e) {
+ data.putString(Apg.EXTRA_ERROR, BaseActivity.this.getString(
+ R.string.error_fileDeleteFailed, file));
+ }
+ Message msg = new Message();
+ msg.setData(data);
+ sendMessage(msg);
+ }
+ });
+ mDeletingThread.start();
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_file);
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ setSecretKeyId(bundle.getLong(Apg.EXTRA_KEY_ID));
+ } else {
+ setSecretKeyId(Id.key.none);
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void setProgress(int resourceId, int progress, int max) {
+ setProgress(getString(resourceId), progress, max);
+ }
+
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt(Constants.extras.STATUS, Id.message.progress_update);
+ data.putInt(Constants.extras.PROGRESS, progress);
+ data.putInt(Constants.extras.PROGRESS_MAX, max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt(Constants.extras.STATUS, Id.message.progress_update);
+ data.putString(Constants.extras.MESSAGE, message);
+ data.putInt(Constants.extras.PROGRESS, progress);
+ data.putInt(Constants.extras.PROGRESS_MAX, max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void handlerCallback(Message msg) {
+ Bundle data = msg.getData();
+ if (data == null) {
+ return;
+ }
+
+ int type = data.getInt(Constants.extras.STATUS);
+ switch (type) {
+ case Id.message.progress_update: {
+ String message = data.getString(Constants.extras.MESSAGE);
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt(Constants.extras.PROGRESS_MAX));
+ mProgressDialog.setProgress(data.getInt(Constants.extras.PROGRESS));
+ }
+ break;
+ }
+
+ case Id.message.delete_done: {
+ mProgressDialog = null;
+ deleteDoneCallback(msg);
+ break;
+ }
+
+ case Id.message.import_done: // intentionally no break
+ case Id.message.export_done: // intentionally no break
+ case Id.message.query_done: // intentionally no break
+ case Id.message.done: {
+ mProgressDialog = null;
+ doneCallback(msg);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ public void doneCallback(Message msg) {
+
+ }
+
+ public void deleteDoneCallback(Message msg) {
+ removeDialog(Id.dialog.deleting);
+ mDeletingThread = null;
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ String message;
+ if (error != null) {
+ message = getString(R.string.errorMessage, error);
+ } else {
+ message = getString(R.string.fileDeleteSuccessful);
+ }
+
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ Apg.setCachedPassPhrase(keyId, passPhrase);
+ }
+
+ public void sendMessage(Message msg) {
+ mHandler.sendMessage(msg);
+ }
+
+ public PausableThread getRunningThread() {
+ return mRunningThread;
+ }
+
+ public void startThread() {
+ mRunningThread = new PausableThread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+
+ }
+
+ public void setSecretKeyId(long id) {
+ mSecretKeyId = id;
+ }
+
+ public long getSecretKeyId() {
+ return mSecretKeyId;
+ }
+
+ protected void setDeleteFile(String deleteFile) {
+ mDeleteFile = deleteFile;
+ }
+
+ protected String getDeleteFile() {
+ return mDeleteFile;
+ }
+
+ public static void setLanguage(Context context, String language) {
+ Locale locale;
+ if (language == null || language.equals("")) {
+ locale = Locale.getDefault();
+ } else {
+ locale = new Locale(language);
+ }
+ Configuration config = new Configuration();
+ config.locale = locale;
+ context.getResources().updateConfiguration(config,
+ context.getResources().getDisplayMetrics());
+ }
+}
diff --git a/org_apg/src/org/apg/ui/DecryptActivity.java b/org_apg/src/org/apg/ui/DecryptActivity.java
new file mode 100644
index 000000000..cb58dfb09
--- /dev/null
+++ b/org_apg/src/org/apg/ui/DecryptActivity.java
@@ -0,0 +1,807 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.DataDestination;
+import org.apg.DataSource;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.PausableThread;
+import org.apg.provider.DataProvider;
+import org.apg.util.Compatibility;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Security;
+import java.security.SignatureException;
+import java.util.regex.Matcher;
+
+public class DecryptActivity extends BaseActivity {
+ private long mSignatureKeyId = 0;
+
+ private Intent mIntent;
+
+ private boolean mReturnResult = false;
+ private String mReplyTo = null;
+ private String mSubject = null;
+ private boolean mSignedOnly = false;
+ private boolean mAssumeSymmetricEncryption = false;
+
+ private EditText mMessage = null;
+ private LinearLayout mSignatureLayout = null;
+ private ImageView mSignatureStatusImage = null;
+ private TextView mUserId = null;
+ private TextView mUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private Button mDecryptButton = null;
+ private Button mReplyButton = null;
+
+ private int mDecryptTarget;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private Uri mContentUri = null;
+ private byte[] mData = null;
+ private boolean mReturnBinary = false;
+
+ private DataSource mDataSource = null;
+ private DataDestination mDataDestination = null;
+
+ private long mUnknownSignatureKeyId = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.decrypt);
+
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mDecryptButton = (Button) findViewById(R.id.btn_decrypt);
+ mReplyButton = (Button) findViewById(R.id.btn_reply);
+ mSignatureLayout = (LinearLayout) findViewById(R.id.signature);
+ mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
+ mUserId = (TextView) findViewById(R.id.mainUserId);
+ mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ openFile();
+ }
+ });
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
+
+ // default: message source
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+
+ mIntent = getIntent();
+ if (Intent.ACTION_VIEW.equals(mIntent.getAction())) {
+ Uri uri = mIntent.getData();
+ try {
+ InputStream attachment = getContentResolver().openInputStream(uri);
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ byte bytes[] = new byte[1 << 16];
+ int length;
+ while ((length = attachment.read(bytes)) > 0) {
+ byteOut.write(bytes, 0, length);
+ }
+ byteOut.close();
+ String data = new String(byteOut.toByteArray());
+ mMessage.setText(data);
+ } catch (FileNotFoundException e) {
+ // ignore, then
+ } catch (IOException e) {
+ // ignore, then
+ }
+ } else if (Apg.Intent.DECRYPT.equals(mIntent.getAction())) {
+ Log.d(Constants.TAG, "Apg Intent DECRYPT startet");
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ Log.d(Constants.TAG, "extra bundle was null");
+ extras = new Bundle();
+ } else {
+ Log.d(Constants.TAG, "got extras");
+ }
+
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ Log.d(Constants.TAG, "EXTRA_DATA was null");
+ textData = extras.getString(Apg.EXTRA_TEXT);
+ } else {
+ Log.d(Constants.TAG, "Got data from EXTRA_DATA");
+ }
+ if (textData != null) {
+ Log.d(Constants.TAG, "textData null, matching text ...");
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+ mDecryptButton.setText(R.string.btn_verify);
+ } else {
+ Log.d(Constants.TAG, "Nothing matched!");
+ }
+ }
+ }
+ mReplyTo = extras.getString(Apg.EXTRA_REPLY_TO);
+ mSubject = extras.getString(Apg.EXTRA_SUBJECT);
+ } else if (Apg.Intent.DECRYPT_FILE.equals(mIntent.getAction())) {
+ mInputFilename = mIntent.getDataString();
+ if ("file".equals(mIntent.getScheme())) {
+ mInputFilename = Uri.decode(mInputFilename.substring(7));
+ }
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else if (Apg.Intent.DECRYPT_AND_RETURN.equals(mIntent.getAction())) {
+ mContentUri = mIntent.getData();
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ mReturnBinary = extras.getBoolean(Apg.EXTRA_BINARY, false);
+
+ if (mContentUri == null) {
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String data = extras.getString(Apg.EXTRA_TEXT);
+ if (data != null) {
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ mDecryptButton.setText(R.string.btn_verify);
+ }
+ }
+ }
+ }
+ mReturnResult = true;
+ }
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && mMessage.getText().length() == 0) {
+
+ CharSequence clipboardText = Compatibility.getClipboardText(this);
+
+ String data = "";
+ if (clipboardText != null) {
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(clipboardText);
+ if (!matcher.matches()) {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(clipboardText);
+ }
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ mMessage.setText(data);
+ Toast.makeText(this, R.string.usingClipboardContent, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ mSignatureLayout.setVisibility(View.GONE);
+ mSignatureLayout.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (mSignatureKeyId == 0) {
+ return;
+ }
+ PGPPublicKeyRing key = Apg.getPublicKeyRing(mSignatureKeyId);
+ if (key != null) {
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID);
+ intent.putExtra(Apg.EXTRA_KEY_ID, mSignatureKeyId);
+ startActivity(intent);
+ }
+ }
+ });
+
+ mDecryptButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ decryptClicked();
+ }
+ });
+
+ mReplyButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ replyClicked();
+ }
+ });
+ mReplyButton.setVisibility(View.INVISIBLE);
+
+ if (mReturnResult) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateSource();
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && (mMessage.getText().length() > 0 || mData != null || mContentUri != null)) {
+ mDecryptButton.performClick();
+ }
+ }
+
+ private void openFile() {
+ String filename = mFilename.getText().toString();
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType("*/*");
+
+ try {
+ startActivityForResult(intent, Id.request.filename);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(this, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void guessOutputFilename() {
+ mInputFilename = mFilename.getText().toString();
+ File file = new File(mInputFilename);
+ String filename = file.getName();
+ if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) {
+ filename = filename.substring(0, filename.length() - 4);
+ }
+ mOutputFilename = Constants.path.APP_DIR + "/" + filename;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ mDecryptButton.setText(R.string.btn_decrypt);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ mDecryptButton.setText(R.string.btn_decrypt);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ private void decryptClicked() {
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mDecryptTarget = Id.target.file;
+ } else {
+ mDecryptTarget = Id.target.message;
+ }
+ initiateDecryption();
+ }
+
+ private void initiateDecryption() {
+ if (mDecryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (mInputFilename.startsWith("file")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ if (mDecryptTarget == Id.target.message) {
+ String messageData = mMessage.getText().toString();
+ Matcher matcher = Apg.PGP_SIGNED_MESSAGE.matcher(messageData);
+ if (matcher.matches()) {
+ mSignedOnly = true;
+ decryptStart();
+ return;
+ }
+ }
+
+ // else treat it as an decrypted message/file
+ mSignedOnly = false;
+ String error = null;
+ fillDataSource();
+ try {
+ InputData in = mDataSource.getInputData(this, false);
+ try {
+ setSecretKeyId(Apg.getDecryptionKeyId(this, in));
+ if (getSecretKeyId() == Id.key.none) {
+ throw new Apg.GeneralException(getString(R.string.error_noSecretKeyFound));
+ }
+ mAssumeSymmetricEncryption = false;
+ } catch (Apg.NoAsymmetricEncryptionException e) {
+ setSecretKeyId(Id.key.symmetric);
+ in = mDataSource.getInputData(this, false);
+ if (!Apg.hasSymmetricEncryption(this, in)) {
+ throw new Apg.GeneralException(getString(R.string.error_noKnownEncryptionFound));
+ }
+ mAssumeSymmetricEncryption = true;
+ }
+
+ if (getSecretKeyId() == Id.key.symmetric
+ || Apg.getCachedPassPhrase(getSecretKeyId()) == null) {
+ showDialog(Id.dialog.pass_phrase);
+ } else {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ } catch (FileNotFoundException e) {
+ error = getString(R.string.error_fileNotFound);
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ private void replyClicked() {
+ Intent intent = new Intent(this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT);
+ String data = mMessage.getText().toString();
+ data = data.replaceAll("(?m)^", "> ");
+ data = "\n\n" + data;
+ intent.putExtra(Apg.EXTRA_TEXT, data);
+ intent.putExtra(Apg.EXTRA_SUBJECT, "Re: " + mSubject);
+ intent.putExtra(Apg.EXTRA_SEND_TO, mReplyTo);
+ intent.putExtra(Apg.EXTRA_SIGNATURE_KEY_ID, getSecretKeyId());
+ intent.putExtra(Apg.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
+ startActivity(intent);
+ }
+
+ private void askForOutputFilename() {
+ showDialog(Id.dialog.output_filename);
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+
+ private void decryptStart() {
+ showDialog(Id.dialog.decrypting);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Security.addProvider(new BouncyCastleProvider());
+
+ Bundle data = new Bundle();
+ Message msg = new Message();
+ fillDataSource();
+ fillDataDestination();
+ try {
+ InputData in = mDataSource.getInputData(this, true);
+ OutputStream out = mDataDestination.getOutputStream(this);
+
+ if (mSignedOnly) {
+ data = Apg.verifyText(this, in, out, this);
+ } else {
+ data = Apg.decrypt(this, in, out, Apg.getCachedPassPhrase(getSecretKeyId()), this,
+ mAssumeSymmetricEncryption);
+ }
+
+ out.close();
+
+ if (mDataDestination.getStreamFilename() != null) {
+ data.putString(Apg.EXTRA_RESULT_URI, "content://" + DataProvider.AUTHORITY
+ + "/data/" + mDataDestination.getStreamFilename());
+ } else if (mDecryptTarget == Id.target.message) {
+ if (mReturnBinary) {
+ data.putByteArray(Apg.EXTRA_DECRYPTED_DATA,
+ ((ByteArrayOutputStream) out).toByteArray());
+ } else {
+ data.putString(Apg.EXTRA_DECRYPTED_MESSAGE, new String(
+ ((ByteArrayOutputStream) out).toByteArray()));
+ }
+ }
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void handlerCallback(Message msg) {
+ Bundle data = msg.getData();
+ if (data == null) {
+ return;
+ }
+
+ if (data.getInt(Constants.extras.STATUS) == Id.message.unknown_signature_key) {
+ mUnknownSignatureKeyId = data.getLong(Constants.extras.KEY_ID);
+ showDialog(Id.dialog.lookup_unknown_key);
+ return;
+ }
+
+ super.handlerCallback(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ removeDialog(Id.dialog.decrypting);
+ mSignatureKeyId = 0;
+ mSignatureLayout.setVisibility(View.GONE);
+ mReplyButton.setVisibility(View.INVISIBLE);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.decryptionSuccessful, Toast.LENGTH_SHORT).show();
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ switch (mDecryptTarget) {
+ case Id.target.message: {
+ String decryptedMessage = data.getString(Apg.EXTRA_DECRYPTED_MESSAGE);
+ mMessage.setText(decryptedMessage);
+ mMessage.setHorizontallyScrolling(false);
+ mReplyButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ case Id.target.file: {
+ if (mDeleteAfter.isChecked()) {
+ setDeleteFile(mInputFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ break;
+ }
+ }
+
+ if (data.getBoolean(Apg.EXTRA_SIGNATURE)) {
+ String userId = data.getString(Apg.EXTRA_SIGNATURE_USER_ID);
+ mSignatureKeyId = data.getLong(Apg.EXTRA_SIGNATURE_KEY_ID);
+ mUserIdRest.setText("id: " + Apg.getSmallFingerPrint(mSignatureKeyId));
+ if (userId == null) {
+ userId = getResources().getString(R.string.unknownUserId);
+ }
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mUserIdRest.setText("<" + chunks[1]);
+ }
+ mUserId.setText(userId);
+
+ if (data.getBoolean(Apg.EXTRA_SIGNATURE_SUCCESS)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ } else if (data.getBoolean(Apg.EXTRA_SIGNATURE_UNKNOWN)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ Toast.makeText(this, R.string.unknownSignatureKeyTouchToLookUp, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ mFilename.setText(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.look_up_key_id: {
+ PausableThread thread = getRunningThread();
+ if (thread != null && thread.isPaused()) {
+ thread.unpause();
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.output_filename: {
+ return FileDialog.build(this, getString(R.string.title_decryptToFile),
+ getString(R.string.specifyFileToDecryptTo), mOutputFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.output_filename);
+ mOutputFilename = filename;
+ decryptStart();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.output_filename);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.output_filename);
+ }
+
+ case Id.dialog.lookup_unknown_key: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.title_unknownSignatureKey);
+ alert.setMessage(getString(R.string.lookupUnknownKey,
+ Apg.getSmallFingerPrint(mUnknownSignatureKeyId)));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.lookup_unknown_key);
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID);
+ intent.putExtra(Apg.EXTRA_KEY_ID, mUnknownSignatureKeyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.lookup_unknown_key);
+ PausableThread thread = getRunningThread();
+ if (thread != null && thread.isPaused()) {
+ thread.unpause();
+ }
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ protected void fillDataSource() {
+ mDataSource = new DataSource();
+ if (mContentUri != null) {
+ mDataSource.setUri(mContentUri);
+ } else if (mDecryptTarget == Id.target.file) {
+ mDataSource.setUri(mInputFilename);
+ } else {
+ if (mData != null) {
+ mDataSource.setData(mData);
+ } else {
+ mDataSource.setText(mMessage.getText().toString());
+ }
+ }
+ }
+
+ protected void fillDataDestination() {
+ mDataDestination = new DataDestination();
+ if (mContentUri != null) {
+ mDataDestination.setMode(Id.mode.stream);
+ } else if (mDecryptTarget == Id.target.file) {
+ mDataDestination.setFilename(mOutputFilename);
+ mDataDestination.setMode(Id.mode.file);
+ } else {
+ mDataDestination.setMode(Id.mode.byte_array);
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/EditKeyActivity.java b/org_apg/src/org/apg/ui/EditKeyActivity.java
new file mode 100644
index 000000000..c3945d4ed
--- /dev/null
+++ b/org_apg/src/org/apg/ui/EditKeyActivity.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.provider.Database;
+import org.apg.ui.widget.KeyEditor;
+import org.apg.ui.widget.SectionView;
+import org.apg.util.IterableIterator;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+public class EditKeyActivity extends BaseActivity implements OnClickListener {
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIds;
+ private SectionView mKeys;
+
+ private Button mSaveButton;
+ private Button mDiscardButton;
+
+ private String mCurrentPassPhrase = null;
+ private String mNewPassPhrase = null;
+
+ private Button mChangePassPhrase;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.edit_key);
+
+ Vector<String> userIds = new Vector<String>();
+ Vector<PGPSecretKey> keys = new Vector<PGPSecretKey>();
+
+ Intent intent = getIntent();
+ long keyId = 0;
+ if (intent.getExtras() != null) {
+ keyId = intent.getExtras().getLong(Apg.EXTRA_KEY_ID);
+ }
+
+ if (keyId != 0) {
+ PGPSecretKey masterKey = null;
+ mKeyRing = Apg.getSecretKeyRing(keyId);
+ if (mKeyRing != null) {
+ masterKey = Apg.getMasterKey(mKeyRing);
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(mKeyRing.getSecretKeys())) {
+ keys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ userIds.add(userId);
+ }
+ }
+ }
+
+ mChangePassPhrase = (Button) findViewById(R.id.btn_change_pass_phrase);
+ mChangePassPhrase.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showDialog(Id.dialog.new_pass_phrase);
+ }
+ });
+
+ mSaveButton = (Button) findViewById(R.id.btn_save);
+ mDiscardButton = (Button) findViewById(R.id.btn_discard);
+
+ mSaveButton.setOnClickListener(this);
+ mDiscardButton.setOnClickListener(this);
+
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LinearLayout container = (LinearLayout) findViewById(R.id.container);
+ mUserIds = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mUserIds.setType(Id.type.user_id);
+ mUserIds.setUserIds(userIds);
+ container.addView(mUserIds);
+ mKeys = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeys.setType(Id.type.key);
+ mKeys.setKeys(keys);
+ container.addView(mKeys);
+
+ mCurrentPassPhrase = Apg.getEditPassPhrase();
+ if (mCurrentPassPhrase == null) {
+ mCurrentPassPhrase = "";
+ }
+
+ updatePassPhraseButtonText();
+
+ Toast.makeText(this,
+ getString(R.string.warningMessage, getString(R.string.keyEditingIsBeta)),
+ Toast.LENGTH_LONG).show();
+ }
+
+ private long getMasterKeyId() {
+ if (mKeys.getEditors().getChildCount() == 0) {
+ return 0;
+ }
+ return ((KeyEditor) mKeys.getEditors().getChildAt(0)).getValue().getKeyID();
+ }
+
+ public boolean havePassPhrase() {
+ return (!mCurrentPassPhrase.equals(""))
+ || (mNewPassPhrase != null && !mNewPassPhrase.equals(""));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(0, Id.menu.option.about, 1, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.new_pass_phrase: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ if (havePassPhrase()) {
+ alert.setTitle(R.string.title_changePassPhrase);
+ } else {
+ alert.setTitle(R.string.title_setPassPhrase);
+ }
+ alert.setMessage(R.string.enterPassPhraseTwice);
+
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.pass_phrase, null);
+ final EditText input1 = (EditText) view.findViewById(R.id.passPhrase);
+ final EditText input2 = (EditText) view.findViewById(R.id.passPhraseAgain);
+
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.new_pass_phrase);
+
+ String passPhrase1 = "" + input1.getText();
+ String passPhrase2 = "" + input2.getText();
+ if (!passPhrase1.equals(passPhrase2)) {
+ showDialog(Id.dialog.pass_phrases_do_not_match);
+ return;
+ }
+
+ if (passPhrase1.equals("")) {
+ showDialog(Id.dialog.no_pass_phrase);
+ return;
+ }
+
+ mNewPassPhrase = passPhrase1;
+ updatePassPhraseButtonText();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.new_pass_phrase);
+ }
+ });
+
+ return alert.create();
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void onClick(View v) {
+ if (v == mSaveButton) {
+ // TODO: some warning
+ saveClicked();
+ } else if (v == mDiscardButton) {
+ finish();
+ }
+ }
+
+ private void saveClicked() {
+ if (!havePassPhrase()) {
+ Toast.makeText(this, R.string.setAPassPhrase, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ showDialog(Id.dialog.saving);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ String oldPassPhrase = mCurrentPassPhrase;
+ String newPassPhrase = mNewPassPhrase;
+ if (newPassPhrase == null) {
+ newPassPhrase = oldPassPhrase;
+ }
+ Apg.buildSecretKey(this, mUserIds, mKeys, oldPassPhrase, newPassPhrase, this);
+ Apg.setCachedPassPhrase(getMasterKeyId(), newPassPhrase);
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ } catch (Database.GeneralException e) {
+ error = "" + e;
+ } catch (IOException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ removeDialog(Id.dialog.saving);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(EditKeyActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(EditKeyActivity.this, R.string.keySaved, Toast.LENGTH_SHORT).show();
+ setResult(RESULT_OK);
+ finish();
+ }
+ }
+
+ private void updatePassPhraseButtonText() {
+ mChangePassPhrase.setText(havePassPhrase() ? R.string.btn_changePassPhrase
+ : R.string.btn_setPassPhrase);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/EncryptActivity.java b/org_apg/src/org/apg/ui/EncryptActivity.java
new file mode 100644
index 000000000..e5892a4d5
--- /dev/null
+++ b/org_apg/src/org/apg/ui/EncryptActivity.java
@@ -0,0 +1,998 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.DataDestination;
+import org.apg.DataSource;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.provider.DataProvider;
+import org.apg.util.Choice;
+import org.apg.util.Compatibility;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+public class EncryptActivity extends BaseActivity {
+ private Intent mIntent = null;
+ private String mSubject = null;
+ private String mSendTo = null;
+
+ private long mEncryptionKeyIds[] = null;
+
+ private boolean mReturnResult = false;
+ private EditText mMessage = null;
+ private Button mSelectKeysButton = null;
+ private Button mEncryptButton = null;
+ private Button mEncryptToClipboardButton = null;
+ private CheckBox mSign = null;
+ private TextView mMainUserId = null;
+ private TextView mMainUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private ViewFlipper mMode = null;
+ private TextView mModeLabel = null;
+ private ImageView mModePrevious = null;
+ private ImageView mModeNext = null;
+
+ private int mEncryptTarget;
+
+ private EditText mPassPhrase = null;
+ private EditText mPassPhraseAgain = null;
+ private CheckBox mAsciiArmour = null;
+ private Spinner mFileCompression = null;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private boolean mAsciiArmourDemand = false;
+ private boolean mOverrideAsciiArmour = false;
+ private Uri mContentUri = null;
+ private byte[] mData = null;
+
+ private DataSource mDataSource = null;
+ private DataDestination mDataDestination = null;
+
+ private boolean mGenerateSignature = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.encrypt);
+
+ mGenerateSignature = false;
+
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMode = (ViewFlipper) findViewById(R.id.mode);
+ mModeLabel = (TextView) findViewById(R.id.modeLabel);
+ mModePrevious = (ImageView) findViewById(R.id.modePrevious);
+ mModeNext = (ImageView) findViewById(R.id.modeNext);
+
+ mModePrevious.setClickable(true);
+ mModePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mMode.showPrevious();
+ updateMode();
+ }
+ });
+
+ OnClickListener nextModeClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mMode.showNext();
+ updateMode();
+ }
+ };
+ mModeNext.setOnClickListener(nextModeClickListener);
+
+ mModeLabel.setClickable(true);
+ mModeLabel.setOnClickListener(nextModeClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mSelectKeysButton = (Button) findViewById(R.id.btn_selectEncryptKeys);
+ mEncryptButton = (Button) findViewById(R.id.btn_encrypt);
+ mEncryptToClipboardButton = (Button) findViewById(R.id.btn_encryptToClipboard);
+ mSign = (CheckBox) findViewById(R.id.sign);
+ mMainUserId = (TextView) findViewById(R.id.mainUserId);
+ mMainUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ mPassPhrase = (EditText) findViewById(R.id.passPhrase);
+ mPassPhraseAgain = (EditText) findViewById(R.id.passPhraseAgain);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ openFile();
+ }
+ });
+
+ mFileCompression = (Spinner) findViewById(R.id.fileCompression);
+ Choice[] choices = new Choice[] {
+ new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " ("
+ + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zip, "ZIP (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zlib, "ZLIB (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.bzip2, "BZIP2 (" + getString(R.string.very_slow)
+ + ")"), };
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(this,
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mFileCompression.setAdapter(adapter);
+
+ int defaultFileCompression = mPreferences.getDefaultFileCompression();
+ for (int i = 0; i < choices.length; ++i) {
+ if (choices[i].getId() == defaultFileCompression) {
+ mFileCompression.setSelection(i);
+ break;
+ }
+ }
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterEncryption);
+
+ mAsciiArmour = (CheckBox) findViewById(R.id.asciiArmour);
+ mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
+ mAsciiArmour.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ guessOutputFilename();
+ }
+ });
+
+ mEncryptToClipboardButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ encryptToClipboardClicked();
+ }
+ });
+
+ mEncryptButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ encryptClicked();
+ }
+ });
+
+ mSelectKeysButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ selectPublicKeys();
+ }
+ });
+
+ mSign.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ CheckBox checkBox = (CheckBox) v;
+ if (checkBox.isChecked()) {
+ selectSecretKey();
+ } else {
+ setSecretKeyId(Id.key.none);
+ updateView();
+ }
+ }
+ });
+
+ mIntent = getIntent();
+ if (Apg.Intent.ENCRYPT.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_FILE.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mContentUri = mIntent.getData();
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mReturnResult = true;
+ }
+
+ if (Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mGenerateSignature = true;
+ mOverrideAsciiArmour = true;
+ mAsciiArmourDemand = false;
+ }
+
+ if (extras.containsKey(Apg.EXTRA_ASCII_ARMOUR)) {
+ mAsciiArmourDemand = extras.getBoolean(Apg.EXTRA_ASCII_ARMOUR, true);
+ mOverrideAsciiArmour = true;
+ mAsciiArmour.setChecked(mAsciiArmourDemand);
+ }
+
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ textData = extras.getString(Apg.EXTRA_TEXT);
+ }
+ mSendTo = extras.getString(Apg.EXTRA_SEND_TO);
+ mSubject = extras.getString(Apg.EXTRA_SUBJECT);
+ long signatureKeyId = extras.getLong(Apg.EXTRA_SIGNATURE_KEY_ID);
+ long encryptionKeyIds[] = extras.getLongArray(Apg.EXTRA_ENCRYPTION_KEY_IDS);
+ if (signatureKeyId != 0) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRing(signatureKeyId);
+ PGPSecretKey masterKey = null;
+ if (keyRing != null) {
+ masterKey = Apg.getMasterKey(keyRing);
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = Apg.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ setSecretKeyId(masterKey.getKeyID());
+ }
+ }
+ }
+ }
+
+ if (encryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRing(encryptionKeyIds[i]);
+ PGPPublicKey masterKey = null;
+ if (keyRing == null) {
+ continue;
+ }
+ masterKey = Apg.getMasterKey(keyRing);
+ if (masterKey == null) {
+ continue;
+ }
+ Vector<PGPPublicKey> encryptKeys = Apg.getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ continue;
+ }
+ goodIds.add(masterKey.getKeyID());
+ }
+ if (goodIds.size() > 0) {
+ mEncryptionKeyIds = new long[goodIds.size()];
+ for (int i = 0; i < goodIds.size(); ++i) {
+ mEncryptionKeyIds[i] = goodIds.get(i);
+ }
+ }
+ }
+
+ if (Apg.Intent.ENCRYPT.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ if (textData != null) {
+ mMessage.setText(textData);
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ } else if (Apg.Intent.ENCRYPT_FILE.equals(mIntent.getAction())) {
+ if ("file".equals(mIntent.getScheme())) {
+ mInputFilename = Uri.decode(mIntent.getDataString().replace("file://", ""));
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ }
+ }
+
+ updateView();
+ updateSource();
+ updateMode();
+
+ if (mReturnResult) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateButtons();
+
+ if (mReturnResult
+ && (mMessage.getText().length() > 0 || mData != null || mContentUri != null)
+ && ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) || getSecretKeyId() != 0)) {
+ encryptClicked();
+ }
+ }
+
+ private void openFile() {
+ String filename = mFilename.getText().toString();
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType("*/*");
+
+ try {
+ startActivityForResult(intent, Id.request.filename);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(this, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void guessOutputFilename() {
+ mInputFilename = mFilename.getText().toString();
+ File file = new File(mInputFilename);
+ String ending = (mAsciiArmour.isChecked() ? ".asc" : ".gpg");
+ mOutputFilename = Constants.path.APP_DIR + "/" + file.getName() + ending;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateButtons();
+ }
+
+ private void updateButtons() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mEncryptToClipboardButton.setVisibility(View.INVISIBLE);
+ mEncryptButton.setText(R.string.btn_encrypt);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ if (mReturnResult) {
+ mEncryptToClipboardButton.setVisibility(View.INVISIBLE);
+ } else {
+ mEncryptToClipboardButton.setVisibility(View.VISIBLE);
+ }
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ } else {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ if (getSecretKeyId() == 0) {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(false);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(false);
+ } else {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_sign);
+ } else {
+ mEncryptButton.setText(R.string.btn_signAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_signToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ }
+ } else {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ }
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ private void updateMode() {
+ switch (mMode.getCurrentView().getId()) {
+ case R.id.modeAsymmetric: {
+ mModeLabel.setText(R.string.label_asymmetric);
+ break;
+ }
+
+ case R.id.modeSymmetric: {
+ mModeLabel.setText(R.string.label_symmetric);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateButtons();
+ }
+
+ private void encryptToClipboardClicked() {
+ mEncryptTarget = Id.target.clipboard;
+ initiateEncryption();
+ }
+
+ private void encryptClicked() {
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mEncryptTarget = Id.target.file;
+ } else {
+ mEncryptTarget = Id.target.email;
+ }
+ initiateEncryption();
+ }
+
+ private void initiateEncryption() {
+ if (mEncryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!mInputFilename.startsWith("content")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ // symmetric encryption
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ boolean gotPassPhrase = false;
+ String passPhrase = mPassPhrase.getText().toString();
+ String passPhraseAgain = mPassPhraseAgain.getText().toString();
+ if (!passPhrase.equals(passPhraseAgain)) {
+ Toast.makeText(this, R.string.passPhrasesDoNotMatch, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ gotPassPhrase = (passPhrase.length() != 0);
+ if (!gotPassPhrase) {
+ Toast.makeText(this, R.string.passPhraseMustNotBeEmpty, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } else {
+ boolean encryptIt = (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0);
+ // for now require at least one form of encryption for files
+ if (!encryptIt && mEncryptTarget == Id.target.file) {
+ Toast.makeText(this, R.string.selectEncryptionKey, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!encryptIt && getSecretKeyId() == 0) {
+ Toast.makeText(this, R.string.selectEncryptionOrSignatureKey, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (getSecretKeyId() != 0 && Apg.getCachedPassPhrase(getSecretKeyId()) == null) {
+ showDialog(Id.dialog.pass_phrase);
+ return;
+ }
+ }
+
+ if (mEncryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ encryptStart();
+ }
+ }
+
+ private void askForOutputFilename() {
+ showDialog(Id.dialog.output_filename);
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ if (mEncryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ encryptStart();
+ }
+ }
+
+ private void encryptStart() {
+ showDialog(Id.dialog.encrypting);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ InputData in;
+ OutputStream out;
+ boolean useAsciiArmour = true;
+ long encryptionKeyIds[] = null;
+ long signatureKeyId = 0;
+ int compressionId = 0;
+ boolean signOnly = false;
+
+ String passPhrase = null;
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ passPhrase = mPassPhrase.getText().toString();
+ if (passPhrase.length() == 0) {
+ passPhrase = null;
+ }
+ } else {
+ encryptionKeyIds = mEncryptionKeyIds;
+ signatureKeyId = getSecretKeyId();
+ signOnly = (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0);
+ }
+
+ fillDataSource(signOnly && !mReturnResult);
+ fillDataDestination();
+
+ // streams
+ in = mDataSource.getInputData(this, true);
+ out = mDataDestination.getOutputStream(this);
+
+ if (mEncryptTarget == Id.target.file) {
+ useAsciiArmour = mAsciiArmour.isChecked();
+ compressionId = ((Choice) mFileCompression.getSelectedItem()).getId();
+ } else {
+ useAsciiArmour = true;
+ compressionId = mPreferences.getDefaultMessageCompression();
+ }
+
+ if (mOverrideAsciiArmour) {
+ useAsciiArmour = mAsciiArmourDemand;
+ }
+
+ if (mGenerateSignature) {
+ Apg.generateSignature(this, in, out, useAsciiArmour, mDataSource.isBinary(),
+ getSecretKeyId(), Apg.getCachedPassPhrase(getSecretKeyId()),
+ mPreferences.getDefaultHashAlgorithm(),
+ mPreferences.getForceV3Signatures(), this);
+ } else if (signOnly) {
+ Apg.signText(this, in, out, getSecretKeyId(),
+ Apg.getCachedPassPhrase(getSecretKeyId()),
+ mPreferences.getDefaultHashAlgorithm(),
+ mPreferences.getForceV3Signatures(), this);
+ } else {
+ Apg.encrypt(this, in, out, useAsciiArmour, encryptionKeyIds, signatureKeyId,
+ Apg.getCachedPassPhrase(signatureKeyId), this,
+ mPreferences.getDefaultEncryptionAlgorithm(),
+ mPreferences.getDefaultHashAlgorithm(), compressionId,
+ mPreferences.getForceV3Signatures(), passPhrase);
+ }
+
+ out.close();
+ if (mEncryptTarget != Id.target.file) {
+
+ if (out instanceof ByteArrayOutputStream) {
+ if (useAsciiArmour) {
+ String extraData = new String(((ByteArrayOutputStream) out).toByteArray());
+ if (mGenerateSignature) {
+ data.putString(Apg.EXTRA_SIGNATURE_TEXT, extraData);
+ } else {
+ data.putString(Apg.EXTRA_ENCRYPTED_MESSAGE, extraData);
+ }
+ } else {
+ byte extraData[] = ((ByteArrayOutputStream) out).toByteArray();
+ if (mGenerateSignature) {
+ data.putByteArray(Apg.EXTRA_SIGNATURE_DATA, extraData);
+ } else {
+ data.putByteArray(Apg.EXTRA_ENCRYPTED_DATA, extraData);
+ }
+ }
+ } else if (out instanceof FileOutputStream) {
+ String fileName = mDataDestination.getStreamFilename();
+ String uri = "content://" + DataProvider.AUTHORITY + "/data/" + fileName;
+ data.putString(Apg.EXTRA_RESULT_URI, uri);
+ } else {
+ throw new Apg.GeneralException("No output-data found.");
+ }
+ }
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ private void updateView() {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ mSelectKeysButton.setText(R.string.noKeysSelected);
+ } else if (mEncryptionKeyIds.length == 1) {
+ mSelectKeysButton.setText(R.string.oneKeySelected);
+ } else {
+ mSelectKeysButton.setText("" + mEncryptionKeyIds.length + " "
+ + getResources().getString(R.string.nKeysSelected));
+ }
+
+ if (getSecretKeyId() == 0) {
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknownUserId);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRing(getSecretKeyId());
+ if (keyRing != null) {
+ PGPSecretKey key = Apg.getMasterKey(keyRing);
+ if (key != null) {
+ String userId = Apg.getMainUserIdSafe(this, key);
+ String chunks[] = userId.split(" <", 2);
+ uid = chunks[0];
+ if (chunks.length > 1) {
+ uidExtra = "<" + chunks[1];
+ }
+ }
+ }
+ mMainUserId.setText(uid);
+ mMainUserIdRest.setText(uidExtra);
+ mSign.setChecked(true);
+ }
+
+ updateButtons();
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(this, SelectPublicKeyListActivity.class);
+ Vector<Long> keyIds = new Vector<Long>();
+ if (getSecretKeyId() != 0) {
+ keyIds.add(getSecretKeyId());
+ }
+ if (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) {
+ for (int i = 0; i < mEncryptionKeyIds.length; ++i) {
+ keyIds.add(mEncryptionKeyIds[i]);
+ }
+ }
+ long[] initialKeyIds = null;
+ if (keyIds.size() > 0) {
+ initialKeyIds = new long[keyIds.size()];
+ for (int i = 0; i < keyIds.size(); ++i) {
+ initialKeyIds[i] = keyIds.get(i);
+ }
+ }
+ intent.putExtra(Apg.EXTRA_SELECTION, initialKeyIds);
+ startActivityForResult(intent, Id.request.public_keys);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ mFilename.setText(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ updateView();
+ break;
+ }
+
+ case Id.request.public_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mEncryptionKeyIds = bundle.getLongArray(Apg.EXTRA_SELECTION);
+ }
+ updateView();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.encrypting);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ switch (mEncryptTarget) {
+ case Id.target.clipboard: {
+ String message = data.getString(Apg.EXTRA_ENCRYPTED_MESSAGE);
+ Compatibility.copyToClipboard(this, message);
+ Toast.makeText(this, R.string.encryptionToClipboardSuccessful, Toast.LENGTH_SHORT)
+ .show();
+ break;
+ }
+
+ case Id.target.email: {
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ String message = data.getString(Apg.EXTRA_ENCRYPTED_MESSAGE);
+ Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
+ emailIntent.setType("text/plain; charset=utf-8");
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);
+ if (mSubject != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, mSubject);
+ }
+ if (mSendTo != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] { mSendTo });
+ }
+ EncryptActivity.this.startActivity(Intent.createChooser(emailIntent,
+ getString(R.string.title_sendEmail)));
+ break;
+ }
+
+ case Id.target.file: {
+ Toast.makeText(this, R.string.encryptionSuccessful, Toast.LENGTH_SHORT).show();
+ if (mDeleteAfter.isChecked()) {
+ setDeleteFile(mInputFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.output_filename: {
+ return FileDialog.build(this, getString(R.string.title_encryptToFile),
+ getString(R.string.specifyFileToEncryptTo), mOutputFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.output_filename);
+ mOutputFilename = filename;
+ encryptStart();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.output_filename);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.output_filename);
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ protected void fillDataSource(boolean fixContent) {
+ mDataSource = new DataSource();
+ if (mContentUri != null) {
+ mDataSource.setUri(mContentUri);
+ } else if (mEncryptTarget == Id.target.file) {
+ mDataSource.setUri(mInputFilename);
+ } else {
+ if (mData != null) {
+ mDataSource.setData(mData);
+ } else {
+ String message = mMessage.getText().toString();
+ if (fixContent) {
+ // fix the message a bit, trailing spaces and newlines break stuff,
+ // because GMail sends as HTML and such things fuck up the
+ // signature,
+ // TODO: things like "<" and ">" also fuck up the signature
+ message = message.replaceAll(" +\n", "\n");
+ message = message.replaceAll("\n\n+", "\n\n");
+ message = message.replaceFirst("^\n+", "");
+ // make sure there'll be exactly one newline at the end
+ message = message.replaceFirst("\n*$", "\n");
+ }
+ mDataSource.setText(message);
+ }
+ }
+ }
+
+ protected void fillDataDestination() {
+ mDataDestination = new DataDestination();
+ if (mContentUri != null) {
+ mDataDestination.setMode(Id.mode.stream);
+ } else if (mEncryptTarget == Id.target.file) {
+ mDataDestination.setFilename(mOutputFilename);
+ mDataDestination.setMode(Id.mode.file);
+ } else {
+ mDataDestination.setMode(Id.mode.byte_array);
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/GeneralActivity.java b/org_apg/src/org/apg/ui/GeneralActivity.java
new file mode 100644
index 000000000..d70694630
--- /dev/null
+++ b/org_apg/src/org/apg/ui/GeneralActivity.java
@@ -0,0 +1,177 @@
+package org.apg.ui;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.util.Choice;
+import org.apg.R;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class GeneralActivity extends BaseActivity {
+ private Intent mIntent;
+ private ArrayAdapter<Choice> mAdapter;
+ private ListView mList;
+ private Button mCancelButton;
+ private String mDataString;
+ private Uri mDataUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.general);
+
+ mIntent = getIntent();
+
+ InputStream inStream = null;
+ {
+ String data = mIntent.getStringExtra(Intent.EXTRA_TEXT);
+ if (data != null) {
+ mDataString = data;
+ inStream = new ByteArrayInputStream(data.getBytes());
+ }
+ }
+
+ if (inStream == null) {
+ Uri data = mIntent.getData();
+ if (data != null) {
+ mDataUri = data;
+ try {
+ inStream = getContentResolver().openInputStream(data);
+ } catch (FileNotFoundException e) {
+ // didn't work
+ Toast.makeText(this, "failed to open stream", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ if (inStream == null) {
+ Toast.makeText(this, "no data found", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ int contentType = Id.content.unknown;
+ try {
+ contentType = Apg.getStreamContent(this, inStream);
+ inStream.close();
+ } catch (IOException e) {
+ // just means that there's no PGP data in there
+ }
+
+ mList = (ListView) findViewById(R.id.options);
+ Vector<Choice> choices = new Vector<Choice>();
+
+ if (contentType == Id.content.keys) {
+ choices.add(new Choice(Id.choice.action.import_public,
+ getString(R.string.action_importPublic)));
+ choices.add(new Choice(Id.choice.action.import_secret,
+ getString(R.string.action_importSecret)));
+ }
+
+ if (contentType == Id.content.encrypted_data) {
+ choices.add(new Choice(Id.choice.action.decrypt, getString(R.string.action_decrypt)));
+ }
+
+ if (contentType == Id.content.unknown) {
+ choices.add(new Choice(Id.choice.action.encrypt, getString(R.string.action_encrypt)));
+ }
+
+ mAdapter = new ArrayAdapter<Choice>(this, android.R.layout.simple_list_item_1, choices);
+ mList.setAdapter(mAdapter);
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
+ clicked(mAdapter.getItem(arg2).getId());
+ }
+ });
+
+ mCancelButton = (Button) findViewById(R.id.btn_cancel);
+ mCancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ GeneralActivity.this.finish();
+ }
+ });
+
+ if (choices.size() == 1) {
+ clicked(choices.get(0).getId());
+ }
+ }
+
+ private void clicked(int id) {
+ Intent intent = new Intent();
+ switch (id) {
+ case Id.choice.action.encrypt: {
+ intent.setClass(this, EncryptActivity.class);
+ if (mDataString != null) {
+ intent.setAction(Apg.Intent.ENCRYPT);
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setAction(Apg.Intent.ENCRYPT_FILE);
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+
+ break;
+ }
+
+ case Id.choice.action.decrypt: {
+ intent.setClass(this, DecryptActivity.class);
+ if (mDataString != null) {
+ intent.setAction(Apg.Intent.DECRYPT);
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setAction(Apg.Intent.DECRYPT_FILE);
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+
+ break;
+ }
+
+ case Id.choice.action.import_public: {
+ intent.setClass(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ if (mDataString != null) {
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+ break;
+ }
+
+ case Id.choice.action.import_secret: {
+ intent.setClass(this, SecretKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ if (mDataString != null) {
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ return;
+ }
+ }
+
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java b/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java
new file mode 100644
index 000000000..593c841df
--- /dev/null
+++ b/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java
@@ -0,0 +1,138 @@
+package org.apg.ui;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.KeyServer.QueryException;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+public class ImportFromQRCodeActivity extends BaseActivity {
+ private static final String TAG = "ImportFromQRCodeActivity";
+
+ private final Bundle status = new Bundle();
+ private final Message msg = new Message();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ private void importAndSign(final long keyId, final String expectedFingerprint) {
+ if (expectedFingerprint != null && expectedFingerprint.length() > 0) {
+
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // TODO: display some sort of spinner here while the user waits
+
+ HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]); // TODO: there should be only 1
+ String encodedKey = server.get(keyId);
+
+ PGPKeyRing keyring = Apg.decodeKeyRing(new ByteArrayInputStream(encodedKey.getBytes()));
+ if (keyring != null && keyring instanceof PGPPublicKeyRing) {
+ PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+
+ // make sure the fingerprints match before we cache this thing
+ String actualFingerprint = Apg.convertToHex(publicKeyRing.getPublicKey().getFingerprint());
+ if (expectedFingerprint.equals(actualFingerprint)) {
+ // store the signed key in our local cache
+ int retval = Apg.storeKeyRingInCache(publicKeyRing);
+ if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
+ status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
+ } else {
+ Intent intent = new Intent(ImportFromQRCodeActivity.this, SignKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.request.sign_key);
+ }
+ } else {
+ status.putString(Apg.EXTRA_ERROR, "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key.");
+ }
+ }
+ } catch (QueryException e) {
+ Log.e(TAG, "Failed to query KeyServer", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to query KeyServer");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to query KeyServer", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to query KeyServer");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ }
+ }
+ };
+
+ t.setName("KeyExchange Download Thread");
+ t.setDaemon(true);
+ t.start();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case IntentIntegrator.REQUEST_CODE: {
+ boolean debug = true; // TODO: remove this!!!
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
+ if (debug || (scanResult != null && scanResult.getFormatName() != null)) {
+ String[] bits = debug ? new String[] { "5993515643896327656", "0816 F68A 6816 68FB 01BF 2CA5 532D 3EB9 1E2F EDE8" } : scanResult.getContents().split(",");
+ if (bits.length != 2) {
+ return; // dont know how to handle this. Not a valid code
+ }
+
+ long keyId = Long.parseLong(bits[0]);
+ String expectedFingerprint = bits[1];
+
+ importAndSign(keyId, expectedFingerprint);
+
+ break;
+ }
+ }
+
+ case Id.request.sign_key: {
+ // signals the end of processing. Signature was either applied, or it wasnt
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show(); // TODO
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyListActivity.java b/org_apg/src/org/apg/ui/KeyListActivity.java
new file mode 100644
index 000000000..6c76f02bc
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyListActivity.java
@@ -0,0 +1,768 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.Button;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Vector;
+
+public class KeyListActivity extends BaseActivity {
+ protected ExpandableListView mList;
+ protected KeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ protected int mSelectedItem = -1;
+ protected int mTask = 0;
+
+ protected String mImportFilename = Constants.path.APP_DIR + "/";
+ protected String mExportFilename = Constants.path.APP_DIR + "/";
+
+ protected String mImportData;
+ protected boolean mDeleteAfterImport = false;
+
+ protected int mKeyType = Id.type.public_key;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_list);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mList = (ExpandableListView) findViewById(R.id.list);
+ registerForContextMenu(mList);
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ protected void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+ mListAdapter = new KeyListAdapter(this, searchString);
+ mList.setAdapter(mListAdapter);
+
+ if (Apg.Intent.IMPORT.equals(intent.getAction())) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = Uri.decode(intent.getDataString().replace("file://", ""));
+ } else {
+ mImportData = intent.getStringExtra(Apg.EXTRA_TEXT);
+ }
+ importKeys();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.import_keys: {
+ showDialog(Id.dialog.import_keys);
+ return true;
+ }
+
+ case Id.menu.option.export_keys: {
+ showDialog(Id.dialog.export_keys);
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuItem.getMenuInfo();
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type != ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ return super.onContextItemSelected(menuItem);
+ }
+
+ switch (menuItem.getItemId()) {
+ case Id.menu.export: {
+ mSelectedItem = groupPosition;
+ showDialog(Id.dialog.export_key);
+ return true;
+ }
+
+ case Id.menu.delete: {
+ mSelectedItem = groupPosition;
+ showDialog(Id.dialog.delete_key);
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ boolean singleKeyExport = false;
+
+ switch (id) {
+ case Id.dialog.delete_key: {
+ final int keyRingId = mListAdapter.getKeyRingId(mSelectedItem);
+ mSelectedItem = -1;
+ // TODO: better way to do this?
+ String userId = "<unknown>";
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null) {
+ if (keyRing instanceof PGPPublicKeyRing) {
+ userId = Apg.getMainUserIdSafe(this,
+ Apg.getMasterKey((PGPPublicKeyRing) keyRing));
+ } else {
+ userId = Apg.getMainUserIdSafe(this,
+ Apg.getMasterKey((PGPSecretKeyRing) keyRing));
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.warning);
+ builder.setMessage(getString(
+ mKeyType == Id.type.public_key ? R.string.keyDeletionConfirmation
+ : R.string.secretKeyDeletionConfirmation, userId));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ deleteKey(keyRingId);
+ removeDialog(Id.dialog.delete_key);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_key);
+ }
+ });
+ return builder.create();
+ }
+
+ case Id.dialog.import_keys: {
+ return FileDialog.build(this, getString(R.string.title_importKeys),
+ getString(R.string.specifyFileToImportFrom), mImportFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.import_keys);
+ mDeleteAfterImport = checked;
+ mImportFilename = filename;
+ importKeys();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.import_keys);
+ }
+ }, getString(R.string.filemanager_titleOpen),
+ getString(R.string.filemanager_btnOpen),
+ getString(R.string.label_deleteAfterImport), Id.request.filename);
+ }
+
+ case Id.dialog.export_key: {
+ singleKeyExport = true;
+ // break intentionally omitted, to use the Id.dialog.export_keys dialog
+ }
+
+ case Id.dialog.export_keys: {
+ String title = (singleKeyExport ? getString(R.string.title_exportKey)
+ : getString(R.string.title_exportKeys));
+
+ final int thisDialogId = (singleKeyExport ? Id.dialog.export_key
+ : Id.dialog.export_keys);
+
+ return FileDialog.build(this, title,
+ getString(mKeyType == Id.type.public_key ? R.string.specifyFileToExportTo
+ : R.string.specifyFileToExportSecretKeysTo), mExportFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(thisDialogId);
+ mExportFilename = filename;
+ exportKeys();
+ }
+
+ public void onCancelClick() {
+ removeDialog(thisDialogId);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.filename);
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void importKeys() {
+ showDialog(Id.dialog.importing);
+ mTask = Id.task.import_keys;
+ startThread();
+ }
+
+ public void exportKeys() {
+ showDialog(Id.dialog.exporting);
+ mTask = Id.task.export_keys;
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ InputStream importInputStream = null;
+ OutputStream exportOutputStream = null;
+ long size = 0;
+ if (mTask == Id.task.import_keys) {
+ if (mImportData != null) {
+ byte[] bytes = mImportData.getBytes();
+ size = bytes.length;
+ importInputStream = new ByteArrayInputStream(bytes);
+ } else {
+ File file = new File(mImportFilename);
+ size = file.length();
+ importInputStream = new FileInputStream(file);
+ }
+ } else {
+ exportOutputStream = new FileOutputStream(mExportFilename);
+ }
+
+ if (mTask == Id.task.import_keys) {
+ data = Apg.importKeyRings(this, mKeyType, new InputData(importInputStream, size),
+ this);
+ } else {
+ Vector<Integer> keyRingIds = new Vector<Integer>();
+ if (mSelectedItem == -1) {
+ keyRingIds = Apg
+ .getKeyRingIds(mKeyType == Id.type.public_key ? Id.database.type_public
+ : Id.database.type_secret);
+ } else {
+ int keyRingId = mListAdapter.getKeyRingId(mSelectedItem);
+ keyRingIds.add(keyRingId);
+ mSelectedItem = -1;
+ }
+ data = Apg.exportKeyRings(this, keyRingIds, exportOutputStream, this);
+ }
+ } catch (FileNotFoundException e) {
+ error = getString(R.string.error_fileNotFound);
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ mImportData = null;
+
+ if (mTask == Id.task.import_keys) {
+ data.putInt(Constants.extras.STATUS, Id.message.import_done);
+ } else {
+ data.putInt(Constants.extras.STATUS, Id.message.export_done);
+ }
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ protected void deleteKey(int keyRingId) {
+ Apg.deleteKey(keyRingId);
+ refreshList();
+ }
+
+ protected void refreshList() {
+ mListAdapter.rebuild(true);
+ mListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt(Constants.extras.STATUS);
+ switch (type) {
+ case Id.message.import_done: {
+ removeDialog(Id.dialog.importing);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(KeyListActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int added = data.getInt("added");
+ int updated = data.getInt("updated");
+ int bad = data.getInt("bad");
+ String message;
+ if (added > 0 && updated > 0) {
+ message = getString(R.string.keysAddedAndUpdated, added, updated);
+ } else if (added > 0) {
+ message = getString(R.string.keysAdded, added);
+ } else if (updated > 0) {
+ message = getString(R.string.keysUpdated, updated);
+ } else {
+ message = getString(R.string.noKeysAddedOrUpdated);
+ }
+ Toast.makeText(KeyListActivity.this, message, Toast.LENGTH_SHORT).show();
+ if (bad > 0) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.badKeysEncountered, bad));
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ alert.setCancelable(true);
+ alert.create().show();
+ } else if (mDeleteAfterImport) {
+ // everything went well, so now delete, if that was turned on
+ setDeleteFile(mImportFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ }
+ refreshList();
+ break;
+ }
+
+ case Id.message.export_done: {
+ removeDialog(Id.dialog.exporting);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(KeyListActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int exported = data.getInt("exported");
+ String message;
+ if (exported == 1) {
+ message = getString(R.string.keyExported);
+ } else if (exported > 0) {
+ message = getString(R.string.keysExported, exported);
+ } else {
+ message = getString(R.string.noKeysExported);
+ }
+ Toast.makeText(KeyListActivity.this, message, Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+
+ protected class KeyListAdapter extends BaseExpandableListAdapter {
+ private LayoutInflater mInflater;
+ private Vector<Vector<KeyChild>> mChildren;
+ private SQLiteDatabase mDatabase;
+ private Cursor mCursor;
+ private String mSearchString;
+
+ private class KeyChild {
+ public static final int KEY = 0;
+ public static final int USER_ID = 1;
+ public static final int FINGER_PRINT = 2;
+
+ public int type;
+ public String userId;
+ public long keyId;
+ public boolean isMasterKey;
+ public int algorithm;
+ public int keySize;
+ public boolean canSign;
+ public boolean canEncrypt;
+ public String fingerPrint;
+
+ public KeyChild(long keyId, boolean isMasterKey, int algorithm, int keySize,
+ boolean canSign, boolean canEncrypt) {
+ this.type = KEY;
+ this.keyId = keyId;
+ this.isMasterKey = isMasterKey;
+ this.algorithm = algorithm;
+ this.keySize = keySize;
+ this.canSign = canSign;
+ this.canEncrypt = canEncrypt;
+ }
+
+ public KeyChild(String userId) {
+ type = USER_ID;
+ this.userId = userId;
+ }
+
+ public KeyChild(String fingerPrint, boolean isFingerPrint) {
+ type = FINGER_PRINT;
+ this.fingerPrint = fingerPrint;
+ }
+ }
+
+ public KeyListAdapter(Context context, String searchString) {
+ mSearchString = searchString;
+
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDatabase = Apg.getDatabase().db();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " + "("
+ + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + Keys.TABLE_NAME + "."
+ + Keys.KEY_RING_ID + " AND " + Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY
+ + " = '1'" + ") " + " INNER JOIN " + UserIds.TABLE_NAME + " ON " + "("
+ + Keys.TABLE_NAME + "." + Keys._ID + " = " + UserIds.TABLE_NAME + "."
+ + UserIds.KEY_ID + " AND " + UserIds.TABLE_NAME + "." + UserIds.RANK
+ + " = '0')");
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
+ + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME
+ + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere(")");
+ }
+
+ mCursor = qb.query(mDatabase, new String[] { KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ }, KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?", new String[] { ""
+ + (mKeyType == Id.type.public_key ? Id.database.type_public
+ : Id.database.type_secret) }, null, null, UserIds.TABLE_NAME + "."
+ + UserIds.USER_ID + " ASC");
+
+ // content provider way for reference, might have to go back to it sometime:
+ /*
+ * Uri contentUri = null; if (mKeyType == Id.type.secret_key) { contentUri =
+ * Apg.CONTENT_URI_SECRET_KEY_RINGS; } else { contentUri =
+ * Apg.CONTENT_URI_PUBLIC_KEY_RINGS; } mCursor = getContentResolver().query( contentUri,
+ * new String[] { DataProvider._ID, // 0 DataProvider.MASTER_KEY_ID, // 1
+ * DataProvider.USER_ID, // 2 }, null, null, null);
+ */
+
+ startManagingCursor(mCursor);
+ rebuild(false);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ public void rebuild(boolean requery) {
+ if (requery) {
+ mCursor.requery();
+ }
+ mChildren = new Vector<Vector<KeyChild>>();
+ for (int i = 0; i < mCursor.getCount(); ++i) {
+ mChildren.add(null);
+ }
+ }
+
+ protected Vector<KeyChild> getChildrenOfGroup(int groupPosition) {
+ Vector<KeyChild> children = mChildren.get(groupPosition);
+ if (children != null) {
+ return children;
+ }
+
+ mCursor.moveToPosition(groupPosition);
+ children = new Vector<KeyChild>();
+ Cursor c = mDatabase.query(Keys.TABLE_NAME, new String[] { Keys._ID, // 0
+ Keys.KEY_ID, // 1
+ Keys.IS_MASTER_KEY, // 2
+ Keys.ALGORITHM, // 3
+ Keys.KEY_SIZE, // 4
+ Keys.CAN_SIGN, // 5
+ Keys.CAN_ENCRYPT, // 6
+ }, Keys.KEY_RING_ID + " = ?", new String[] { mCursor.getString(0) }, null, null,
+ Keys.RANK + " ASC");
+
+ int masterKeyId = -1;
+ long fingerPrintId = -1;
+ for (int i = 0; i < c.getCount(); ++i) {
+ c.moveToPosition(i);
+ children.add(new KeyChild(c.getLong(1), c.getInt(2) == 1, c.getInt(3), c.getInt(4),
+ c.getInt(5) == 1, c.getInt(6) == 1));
+ if (i == 0) {
+ masterKeyId = c.getInt(0);
+ fingerPrintId = c.getLong(1);
+ }
+ }
+ c.close();
+
+ if (masterKeyId != -1) {
+ children.insertElementAt(new KeyChild(Apg.getFingerPrint(fingerPrintId), true), 0);
+ c = mDatabase.query(UserIds.TABLE_NAME, new String[] { UserIds.USER_ID, // 0
+ }, UserIds.KEY_ID + " = ? AND " + UserIds.RANK + " > 0", new String[] { ""
+ + masterKeyId }, null, null, UserIds.RANK + " ASC");
+
+ for (int i = 0; i < c.getCount(); ++i) {
+ c.moveToPosition(i);
+ children.add(new KeyChild(c.getString(0)));
+ }
+ c.close();
+ }
+
+ mChildren.set(groupPosition, children);
+ return children;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ return null;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return getChildrenOfGroup(groupPosition).size();
+ }
+
+ public Object getGroup(int position) {
+ return position;
+ }
+
+ public long getGroupId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public int getKeyRingId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(0); // _ID
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ mCursor.moveToPosition(groupPosition);
+
+ View view = mInflater.inflate(R.layout.key_list_group_item, null);
+ view.setBackgroundResource(android.R.drawable.list_selector_background);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText("");
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+
+ String userId = mCursor.getString(2); // USER_ID
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ if (mainUserId.getText().length() == 0) {
+ mainUserId.setText(R.string.unknownUserId);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ return view;
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(groupPosition);
+
+ Vector<KeyChild> children = getChildrenOfGroup(groupPosition);
+
+ KeyChild child = children.get(childPosition);
+ View view = null;
+ switch (child.type) {
+ case KeyChild.KEY: {
+ if (child.isMasterKey) {
+ view = mInflater.inflate(R.layout.key_list_child_item_master_key, null);
+ } else {
+ view = mInflater.inflate(R.layout.key_list_child_item_sub_key, null);
+ }
+
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ String keyIdStr = Apg.getSmallFingerPrint(child.keyId);
+ keyId.setText(keyIdStr);
+ TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
+ String algorithmStr = Apg.getAlgorithmInfo(child.algorithm, child.keySize);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
+ if (!child.canEncrypt) {
+ encryptIcon.setVisibility(View.GONE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
+ if (!child.canSign) {
+ signIcon.setVisibility(View.GONE);
+ }
+ break;
+ }
+
+ case KeyChild.USER_ID: {
+ view = mInflater.inflate(R.layout.key_list_child_item_user_id, null);
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ userId.setText(child.userId);
+ break;
+ }
+
+ case KeyChild.FINGER_PRINT: {
+ view = mInflater.inflate(R.layout.key_list_child_item_user_id, null);
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ userId.setText(getString(R.string.fingerprint) + ":\n"
+ + child.fingerPrint.replace(" ", "\n"));
+ break;
+ }
+ }
+ return view;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java b/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java
new file mode 100644
index 000000000..85d31779a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.ui.widget.Editor;
+import org.apg.ui.widget.KeyServerEditor;
+import org.apg.ui.widget.Editor.EditorListener;
+import org.apg.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class KeyServerPreferenceActivity extends BaseActivity
+ implements OnClickListener, EditorListener {
+ private LayoutInflater mInflater;
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+ private TextView mSummary;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_server_preference);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mTitle = (TextView) findViewById(R.id.title);
+ mSummary = (TextView) findViewById(R.id.summary);
+
+ mTitle.setText(R.string.label_keyServers);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mAdd = findViewById(R.id.add);
+ mAdd.setOnClickListener(this);
+
+ Intent intent = getIntent();
+ String servers[] = intent.getStringArrayExtra(Apg.EXTRA_KEY_SERVERS);
+ if (servers != null) {
+ for (int i = 0; i < servers.length; ++i) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(servers[i]);
+ mEditors.addView(view);
+ }
+ }
+
+ Button okButton = (Button) findViewById(R.id.btn_ok);
+ okButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ okClicked();
+ }
+ });
+
+ Button cancelButton = (Button) findViewById(R.id.btn_cancel);
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ cancelClicked();
+ }
+ });
+ }
+
+ public void onDeleted(Editor editor) {
+ // nothing to do
+ }
+
+ public void onClick(View v) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ mEditors.addView(view);
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<String> servers = new Vector<String>();
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i);
+ String tmp = editor.getValue();
+ if (tmp.length() > 0) {
+ servers.add(tmp);
+ }
+ }
+ String[] dummy = new String[0];
+ data.putExtra(Apg.EXTRA_KEY_SERVERS, servers.toArray(dummy));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // override this, so no option menu is added (as would be in BaseActivity), since
+ // we're still in preferences
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyServerQueryActivity.java b/org_apg/src/org/apg/ui/KeyServerQueryActivity.java
new file mode 100644
index 000000000..606acb575
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyServerQueryActivity.java
@@ -0,0 +1,297 @@
+package org.apg.ui;
+
+import java.util.List;
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.KeyServer.InsufficientQuery;
+import org.apg.KeyServer.KeyInfo;
+import org.apg.KeyServer.QueryException;
+import org.apg.KeyServer.TooManyResponses;
+import org.apg.R;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class KeyServerQueryActivity extends BaseActivity {
+ private ListView mList;
+ private EditText mQuery;
+ private Button mSearch;
+ private KeyInfoListAdapter mAdapter;
+ private Spinner mKeyServer;
+
+ private int mQueryType;
+ private String mQueryString;
+ private long mQueryId;
+ private volatile List<KeyInfo> mSearchResult;
+ private volatile String mKeyData;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_query_layout);
+
+ mQuery = (EditText) findViewById(R.id.query);
+ mSearch = (Button) findViewById(R.id.btn_search);
+ mList = (ListView) findViewById(R.id.list);
+ mAdapter = new KeyInfoListAdapter(this);
+ mList.setAdapter(mAdapter);
+
+ mKeyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item,
+ mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mKeyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ mKeyServer.setSelection(0);
+ } else {
+ mSearch.setEnabled(false);
+ }
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) {
+ get(keyId);
+ }
+ });
+
+ mSearch.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ String query = mQuery.getText().toString();
+ search(query);
+ }
+ });
+
+ Intent intent = getIntent();
+ if (Apg.Intent.LOOK_UP_KEY_ID.equals(intent.getAction()) ||
+ Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(intent.getAction())) {
+ long keyId = intent.getLongExtra(Apg.EXTRA_KEY_ID, 0);
+ if (keyId != 0) {
+ String query = "0x" + Apg.keyToHex(keyId);
+ mQuery.setText(query);
+ search(query);
+ }
+ }
+ }
+
+ private void search(String query) {
+ showDialog(Id.dialog.querying);
+ mQueryType = Id.keyserver.search;
+ mQueryString = query;
+ mAdapter.setKeys(new Vector<KeyInfo>());
+ startThread();
+ }
+
+ private void get(long keyId) {
+ showDialog(Id.dialog.querying);
+ mQueryType = Id.keyserver.get;
+ mQueryId = keyId;
+ startThread();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ ProgressDialog progress = (ProgressDialog) super.onCreateDialog(id);
+ progress.setMessage(this.getString(R.string.progress_queryingServer,
+ (String)mKeyServer.getSelectedItem()));
+ return progress;
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ HkpKeyServer server = new HkpKeyServer((String)mKeyServer.getSelectedItem());
+ if (mQueryType == Id.keyserver.search) {
+ mSearchResult = server.search(mQueryString);
+ } else if (mQueryType == Id.keyserver.get) {
+ mKeyData = server.get(mQueryId);
+ }
+ } catch (QueryException e) {
+ error = "" + e;
+ } catch (InsufficientQuery e) {
+ error = "Insufficient query.";
+ } catch (TooManyResponses e) {
+ error = "Too many responses.";
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.querying);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (mQueryType == Id.keyserver.search) {
+ if (mSearchResult != null) {
+ Toast.makeText(this, getString(R.string.keysFound, mSearchResult.size()), Toast.LENGTH_SHORT).show();
+ mAdapter.setKeys(mSearchResult);
+ }
+ } else if (mQueryType == Id.keyserver.get) {
+ Intent orgIntent = getIntent();
+ if (Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
+ if (mKeyData != null) {
+ Intent intent = new Intent();
+ intent.putExtra(Apg.EXTRA_TEXT, mKeyData);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ } else {
+ if (mKeyData != null) {
+ Intent intent = new Intent(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ intent.putExtra(Apg.EXTRA_TEXT, mKeyData);
+ startActivity(intent);
+ }
+ }
+ }
+ }
+
+ public class KeyInfoListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected Activity mActivity;
+ protected List<KeyInfo> mKeys;
+
+ public KeyInfoListAdapter(Activity activity) {
+ mActivity = activity;
+ mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mKeys = new Vector<KeyInfo>();
+ }
+
+ public void setKeys(List<KeyInfo> keys) {
+ mKeys = keys;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mKeys.size();
+ }
+
+ public Object getItem(int position) {
+ return mKeys.get(position);
+ }
+
+ public long getItemId(int position) {
+ return mKeys.get(position).keyId;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ KeyInfo keyInfo = mKeys.get(position);
+
+ View view = mInflater.inflate(R.layout.key_server_query_result_item, null);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
+ algorithm.setText("");
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText("");
+
+ String userId = keyInfo.userIds.get(0);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ keyId.setText(Apg.getSmallFingerPrint(keyInfo.keyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm);
+
+ if (keyInfo.revoked != null) {
+ status.setText("revoked");
+ } else {
+ status.setVisibility(View.GONE);
+ }
+
+ LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
+ if (keyInfo.userIds.size() == 1) {
+ ll.setVisibility(View.GONE);
+ } else {
+ boolean first = true;
+ boolean second = true;
+ for (String uid : keyInfo.userIds) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (!second) {
+ View sep = new View(mActivity);
+ sep.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, 1));
+ sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
+ ll.addView(sep);
+ }
+ TextView uidView = (TextView) mInflater.inflate(R.layout.key_server_query_result_user_id, null);
+ uidView.setText(uid);
+ ll.addView(uidView);
+ second = false;
+ }
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/MailListActivity.java b/org_apg/src/org/apg/ui/MailListActivity.java
new file mode 100644
index 000000000..ad1d08068
--- /dev/null
+++ b/org_apg/src/org/apg/ui/MailListActivity.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.util.Vector;
+import java.util.regex.Matcher;
+
+import org.apg.Apg;
+import org.apg.Preferences;
+import org.apg.R;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+public class MailListActivity extends ListActivity {
+ LayoutInflater mInflater = null;
+
+ public static final String EXTRA_ACCOUNT = "account";
+
+ private static class Conversation {
+ public long id;
+ public String subject;
+ public Vector<Message> messages;
+
+ public Conversation(long id, String subject) {
+ this.id = id;
+ this.subject = subject;
+ }
+ }
+
+ private static class Message {
+ public Conversation parent;
+ public long id;
+ public String subject;
+ public String fromAddress;
+ public String data;
+ public String replyTo;
+ public boolean signedOnly;
+
+ public Message(Conversation parent, long id, String subject,
+ String fromAddress, String replyTo,
+ String data, boolean signedOnly) {
+ this.parent = parent;
+ this.id = id;
+ this.subject = subject;
+ this.fromAddress = fromAddress;
+ this.replyTo = replyTo;
+ this.data = data;
+ if (this.replyTo == null || this.replyTo.equals("")) {
+ this.replyTo = this.fromAddress;
+ }
+ this.signedOnly = signedOnly;
+ }
+ }
+
+ private Vector<Conversation> mConversations;
+ private Vector<Message> mMessages;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Preferences prefs = Preferences.getPreferences(this);
+ BaseActivity.setLanguage(this, prefs.getLanguage());
+
+ super.onCreate(savedInstanceState);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mConversations = new Vector<Conversation>();
+ mMessages = new Vector<Message>();
+
+ String account = getIntent().getExtras().getString(EXTRA_ACCOUNT);
+ // TODO: what if account is null?
+ Uri uri = Uri.parse("content://gmail-ls/conversations/" + account);
+ Cursor cursor =
+ managedQuery(uri, new String[] { "conversation_id", "subject" }, null, null, null);
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+
+ int idIndex = cursor.getColumnIndex("conversation_id");
+ int subjectIndex = cursor.getColumnIndex("subject");
+ long conversationId = cursor.getLong(idIndex);
+ Conversation conversation =
+ new Conversation(conversationId, cursor.getString(subjectIndex));
+ Uri messageUri = Uri.withAppendedPath(uri, "" + conversationId + "/messages");
+ Cursor messageCursor =
+ managedQuery(messageUri, new String[] {
+ "messageId",
+ "subject",
+ "fromAddress",
+ "replyToAddresses",
+ "body" }, null, null, null);
+ Vector<Message> messages = new Vector<Message>();
+ for (int j = 0; j < messageCursor.getCount(); ++j) {
+ messageCursor.moveToPosition(j);
+ idIndex = messageCursor.getColumnIndex("messageId");
+ subjectIndex = messageCursor.getColumnIndex("subject");
+ int fromAddressIndex = messageCursor.getColumnIndex("fromAddress");
+ int replyToIndex = messageCursor.getColumnIndex("replyToAddresses");
+ int bodyIndex = messageCursor.getColumnIndex("body");
+ String data = messageCursor.getString(bodyIndex);
+ data = Html.fromHtml(data).toString();
+ boolean signedOnly = false;
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ signedOnly = true;
+ } else {
+ data = null;
+ }
+ }
+ Message message =
+ new Message(conversation,
+ messageCursor.getLong(idIndex),
+ messageCursor.getString(subjectIndex),
+ messageCursor.getString(fromAddressIndex),
+ messageCursor.getString(replyToIndex),
+ data, signedOnly);
+
+ messages.add(message);
+ mMessages.add(message);
+ }
+ conversation.messages = messages;
+ mConversations.add(conversation);
+ }
+
+ setListAdapter(new MailboxAdapter());
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View v, int position, long id) {
+ Intent intent = new Intent(MailListActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT);
+ Message message = (Message) ((MailboxAdapter) getListAdapter()).getItem(position);
+ intent.putExtra(Apg.EXTRA_TEXT, message.data);
+ intent.putExtra(Apg.EXTRA_SUBJECT, message.subject);
+ intent.putExtra(Apg.EXTRA_REPLY_TO, message.replyTo);
+ startActivity(intent);
+ }
+ });
+ }
+
+ private class MailboxAdapter extends BaseAdapter implements ListAdapter {
+
+ @Override
+ public boolean isEnabled(int position) {
+ Message message = (Message) getItem(position);
+ return message.data != null;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mMessages.size();
+ }
+
+ public Object getItem(int position) {
+ return mMessages.get(position);
+ }
+
+ public long getItemId(int position) {
+ return mMessages.get(position).id;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.mailbox_message_item, null);
+
+ Message message = (Message) getItem(position);
+
+ TextView subject = (TextView) view.findViewById(R.id.subject);
+ TextView email = (TextView) view.findViewById(R.id.emailAddress);
+ ImageView status = (ImageView) view.findViewById(R.id.ic_status);
+
+ subject.setText(message.subject);
+ email.setText(message.fromAddress);
+ if (message.data != null) {
+ if (message.signedOnly) {
+ status.setImageResource(R.drawable.signed);
+ } else {
+ status.setImageResource(R.drawable.encrypted);
+ }
+ status.setVisibility(View.VISIBLE);
+ } else {
+ status.setVisibility(View.INVISIBLE);
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/MainActivity.java b/org_apg/src/org/apg/ui/MainActivity.java
new file mode 100644
index 000000000..8c985c2ac
--- /dev/null
+++ b/org_apg/src/org/apg/ui/MainActivity.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.security.Security;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.dialog;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.provider.Accounts;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.util.Linkify;
+import android.text.util.Linkify.TransformFilter;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class MainActivity extends BaseActivity {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private ListView mAccounts = null;
+ private AccountListAdapter mListAdapter = null;
+ private Cursor mAccountCursor;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ Button encryptMessageButton = (Button) findViewById(R.id.btn_encryptMessage);
+ Button decryptMessageButton = (Button) findViewById(R.id.btn_decryptMessage);
+ Button encryptFileButton = (Button) findViewById(R.id.btn_encryptFile);
+ Button decryptFileButton = (Button) findViewById(R.id.btn_decryptFile);
+ mAccounts = (ListView) findViewById(R.id.accounts);
+
+ encryptMessageButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT);
+ startActivity(intent);
+ }
+ });
+
+ decryptMessageButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT);
+ startActivity(intent);
+ }
+ });
+
+ encryptFileButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT_FILE);
+ startActivity(intent);
+ }
+ });
+
+ decryptFileButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT_FILE);
+ startActivity(intent);
+ }
+ });
+
+ mAccountCursor =
+ Apg.getDatabase().db().query(Accounts.TABLE_NAME,
+ new String[] {
+ Accounts._ID,
+ Accounts.NAME,
+ }, null, null, null, null, Accounts.NAME + " ASC");
+ startManagingCursor(mAccountCursor);
+
+ mListAdapter = new AccountListAdapter(this, mAccountCursor);
+ mAccounts.setAdapter(mListAdapter);
+ mAccounts.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View view, int index, long id) {
+ String accountName = (String) mAccounts.getItemAtPosition(index);
+ startActivity(new Intent(MainActivity.this, MailListActivity.class)
+ .putExtra(MailListActivity.EXTRA_ACCOUNT, accountName));
+ }
+ });
+ registerForContextMenu(mAccounts);
+
+ if (!mPreferences.hasSeenHelp()) {
+ showDialog(Id.dialog.help);
+ }
+
+ if (Apg.isReleaseVersion(this) && !mPreferences.hasSeenChangeLog(Apg.getVersion(this))) {
+ showDialog(Id.dialog.change_log);
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.new_account: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle(R.string.title_addAccount);
+ alert.setMessage(R.string.specifyGoogleMailAccount);
+
+ LayoutInflater inflater =
+ (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.add_account_dialog, null);
+
+ final EditText input = (EditText) view.findViewById(R.id.input);
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.new_account);
+ String accountName = "" + input.getText();
+
+ try {
+ Cursor testCursor =
+ managedQuery(Uri.parse("content://gmail-ls/conversations/" +
+ accountName),
+ null, null, null, null);
+ if (testCursor == null) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_accountNotFound,
+ accountName)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (SecurityException e) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_accountReadingNotAllowed)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Accounts.NAME, accountName);
+ try {
+ Apg.getDatabase().db().insert(Accounts.TABLE_NAME,
+ Accounts.NAME, values);
+ mAccountCursor.requery();
+ mListAdapter.notifyDataSetChanged();
+ } catch (SQLException e) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_addingAccountFailed,
+ accountName)),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.new_account);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case Id.dialog.change_log: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Changes " + Apg.getFullVersion(this));
+ LayoutInflater inflater =
+ (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View layout = inflater.inflate(R.layout.info, null);
+ TextView message = (TextView) layout.findViewById(R.id.message);
+
+ message.setText("Changes:\n" +
+ "* \n" +
+ "\n" +
+ "WARNING: be careful editing your existing keys, as they " +
+ "WILL be stripped of certificates right now.\n" +
+ "\n" +
+ "Also: key cross-certification is NOT supported, so signing " +
+ "with those keys will get a warning when the signature is " +
+ "checked.\n" +
+ "\n" +
+ "I hope APG continues to be useful to you, please send " +
+ "bug reports, feature wishes, feedback.");
+ alert.setView(layout);
+
+ alert.setCancelable(false);
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.change_log);
+ mPreferences.setHasSeenChangeLog(
+ Apg.getVersion(MainActivity.this), true);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case Id.dialog.help: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle(R.string.title_help);
+
+ LayoutInflater inflater =
+ (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View layout = inflater.inflate(R.layout.info, null);
+ TextView message = (TextView) layout.findViewById(R.id.message);
+ message.setText(R.string.text_help);
+
+ TransformFilter packageNames = new TransformFilter() {
+ public final String transformUrl(final Matcher match, String url) {
+ String name = match.group(1).toLowerCase();
+ if (name.equals("astro")) {
+ return "com.metago.astro";
+ } else if (name.equals("k-9 mail")) {
+ return "com.fsck.k9";
+ } else {
+ return "org.openintents.filemanager";
+ }
+ }
+ };
+
+ Pattern pattern = Pattern.compile("(OI File Manager|ASTRO|K-9 Mail)");
+ String scheme = "market://search?q=pname:";
+ message.setAutoLinkMask(0);
+ Linkify.addLinks(message, pattern, scheme, null, packageNames);
+
+ alert.setView(layout);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.help);
+ mPreferences.setHasSeenHelp(true);
+ }
+ });
+
+ return alert.create();
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.manage_public_keys, 0, R.string.menu_managePublicKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(0, Id.menu.option.manage_secret_keys, 1, R.string.menu_manageSecretKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(1, Id.menu.option.create, 2, R.string.menu_addAccount)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(2, Id.menu.option.preferences, 3, R.string.menu_preferences)
+ .setIcon(android.R.drawable.ic_menu_preferences);
+ menu.add(2, Id.menu.option.key_server, 4, R.string.menu_keyServer)
+ .setIcon(android.R.drawable.ic_menu_search);
+ menu.add(3, Id.menu.option.about, 5, R.string.menu_about)
+ .setIcon(android.R.drawable.ic_menu_info_details);
+ menu.add(3, Id.menu.option.help, 6, R.string.menu_help)
+ .setIcon(android.R.drawable.ic_menu_help);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ showDialog(Id.dialog.new_account);
+ return true;
+ }
+
+ case Id.menu.option.manage_public_keys: {
+ startActivity(new Intent(this, PublicKeyListActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.manage_secret_keys: {
+ startActivity(new Intent(this, SecretKeyListActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.help: {
+ showDialog(Id.dialog.help);
+ return true;
+ }
+
+ case Id.menu.option.key_server: {
+ startActivity(new Intent(this, KeyServerQueryActivity.class));
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ TextView nameTextView = (TextView) v.findViewById(R.id.accountName);
+ if (nameTextView != null) {
+ menu.setHeaderTitle(nameTextView.getText());
+ menu.add(0, Id.menu.delete, 0, R.string.menu_deleteAccount);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info =
+ (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+
+ switch (menuItem.getItemId()) {
+ case Id.menu.delete: {
+ Apg.getDatabase().db().delete(Accounts.TABLE_NAME,
+ Accounts._ID + " = ?",
+ new String[] { "" + info.id });
+ mAccountCursor.requery();
+ mListAdapter.notifyDataSetChanged();
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+
+ private static class AccountListAdapter extends CursorAdapter {
+ private LayoutInflater mInflater;
+
+ public AccountListAdapter(Context context, Cursor cursor) {
+ super(context, cursor);
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ Cursor c = getCursor();
+ c.moveToPosition(position);
+ return c.getString(c.getColumnIndex(Accounts.NAME));
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount();
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.account_item, null);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView nameTextView = (TextView) view.findViewById(R.id.accountName);
+ int nameIndex = cursor.getColumnIndex(Accounts.NAME);
+ final String account = cursor.getString(nameIndex);
+ nameTextView.setText(account);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/org_apg/src/org/apg/ui/PreferencesActivity.java b/org_apg/src/org/apg/ui/PreferencesActivity.java
new file mode 100644
index 000000000..421c9cc39
--- /dev/null
+++ b/org_apg/src/org/apg/ui/PreferencesActivity.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Preferences;
+import org.apg.Constants.pref;
+import org.apg.Id.choice;
+import org.apg.Id.request;
+import org.apg.Id.choice.compression;
+import org.apg.ui.widget.IntegerListPreference;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Vector;
+
+public class PreferencesActivity extends PreferenceActivity {
+ private ListPreference mLanguage = null;
+ private IntegerListPreference mPassPhraseCacheTtl = null;
+ private IntegerListPreference mEncryptionAlgorithm = null;
+ private IntegerListPreference mHashAlgorithm = null;
+ private IntegerListPreference mMessageCompression = null;
+ private IntegerListPreference mFileCompression = null;
+ private CheckBoxPreference mAsciiArmour = null;
+ private CheckBoxPreference mForceV3Signatures = null;
+ private PreferenceScreen mKeyServerPreference = null;
+ private Preferences mPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mPreferences = Preferences.getPreferences(this);
+ BaseActivity.setLanguage(this, mPreferences.getLanguage());
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.apg_preferences);
+
+ mLanguage = (ListPreference) findPreference(Constants.pref.LANGUAGE);
+ Vector<CharSequence> entryVector = new Vector<CharSequence>(Arrays.asList(mLanguage.getEntries()));
+ Vector<CharSequence> entryValueVector = new Vector<CharSequence>(Arrays.asList(mLanguage.getEntryValues()));
+ String supportedLanguages[] = getResources().getStringArray(R.array.supported_languages);
+ HashSet<String> supportedLanguageSet = new HashSet<String>(Arrays.asList(supportedLanguages));
+ for (int i = entryVector.size() - 1; i > -1; --i)
+ {
+ if (!supportedLanguageSet.contains(entryValueVector.get(i)))
+ {
+ entryVector.remove(i);
+ entryValueVector.remove(i);
+ }
+ }
+ CharSequence dummy[] = new CharSequence[0];
+ mLanguage.setEntries(entryVector.toArray(dummy));
+ mLanguage.setEntryValues(entryValueVector.toArray(dummy));
+ mLanguage.setValue(mPreferences.getLanguage());
+ mLanguage.setSummary(mLanguage.getEntry());
+ mLanguage.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mLanguage.setValue(newValue.toString());
+ mLanguage.setSummary(mLanguage.getEntry());
+ mPreferences.setLanguage(newValue.toString());
+ return false;
+ }
+ });
+
+ mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL);
+ mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPassPhraseCacheTtl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mPassPhraseCacheTtl.setValue(newValue.toString());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString()));
+ BaseActivity.startCacheService(PreferencesActivity.this, mPreferences);
+ return false;
+ }
+ });
+
+ mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM);
+ int valueIds[] = {
+ PGPEncryptedData.AES_128, PGPEncryptedData.AES_192, PGPEncryptedData.AES_256,
+ PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH, PGPEncryptedData.CAST5,
+ PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES, PGPEncryptedData.IDEA,
+ };
+ String entries[] = {
+ "AES-128", "AES-192", "AES-256",
+ "Blowfish", "Twofish", "CAST5",
+ "DES", "Triple DES", "IDEA",
+ };
+ String values[] = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mEncryptionAlgorithm.setEntries(entries);
+ mEncryptionAlgorithm.setEntryValues(values);
+ mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mEncryptionAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mEncryptionAlgorithm.setValue(newValue.toString());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM);
+ valueIds = new int[] {
+ HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160, HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256, HashAlgorithmTags.SHA384,
+ HashAlgorithmTags.SHA512,
+ };
+ entries = new String[] {
+ "MD5", "RIPEMD-160", "SHA-1",
+ "SHA-224", "SHA-256", "SHA-384",
+ "SHA-512",
+ };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mHashAlgorithm.setEntries(entries);
+ mHashAlgorithm.setEntryValues(values);
+ mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mHashAlgorithm.setValue(newValue.toString());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION);
+ valueIds = new int[] {
+ Id.choice.compression.none,
+ Id.choice.compression.zip,
+ Id.choice.compression.zlib,
+ Id.choice.compression.bzip2,
+ };
+ entries = new String[] {
+ getString(R.string.choice_none) + " (" + getString(R.string.fast) + ")",
+ "ZIP (" + getString(R.string.fast) + ")",
+ "ZLIB (" + getString(R.string.fast) + ")",
+ "BZIP2 (" + getString(R.string.very_slow) + ")",
+ };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mMessageCompression.setEntries(entries);
+ mMessageCompression.setEntryValues(values);
+ mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mMessageCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mMessageCompression.setValue(newValue.toString());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION);
+ mFileCompression.setEntries(entries);
+ mFileCompression.setEntryValues(values);
+ mFileCompression.setValue("" + mPreferences.getDefaultFileCompression());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mFileCompression.setValue(newValue.toString());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR);
+ mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
+ mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mAsciiArmour.setChecked((Boolean)newValue);
+ mPreferences.setDefaultAsciiArmour((Boolean)newValue);
+ return false;
+ }
+ });
+
+ mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES);
+ mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures());
+ mForceV3Signatures.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mForceV3Signatures.setChecked((Boolean)newValue);
+ mPreferences.setForceV3Signatures((Boolean)newValue);
+ return false;
+ }
+ });
+
+ mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS);
+ String servers[] = mPreferences.getKeyServers();
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, servers.length));
+ mKeyServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(PreferencesActivity.this,
+ KeyServerPreferenceActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_SERVERS, mPreferences.getKeyServers());
+ startActivityForResult(intent, Id.request.key_server_preference);
+ return false;
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.key_server_preference: {
+ if (resultCode == RESULT_CANCELED || data == null) {
+ return;
+ }
+ String servers[] = data.getStringArrayExtra(Apg.EXTRA_KEY_SERVERS);
+ mPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+}
+
diff --git a/org_apg/src/org/apg/ui/PublicKeyListActivity.java b/org_apg/src/org/apg/ui/PublicKeyListActivity.java
new file mode 100644
index 000000000..81a79ce33
--- /dev/null
+++ b/org_apg/src/org/apg/ui/PublicKeyListActivity.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Constants.path;
+import org.apg.Id.menu;
+import org.apg.Id.request;
+import org.apg.Id.type;
+import org.apg.Id.menu.option;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class PublicKeyListActivity extends KeyListActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mExportFilename = Constants.path.APP_DIR + "/pubexport.asc";
+ mKeyType = Id.type.public_key;
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.import_keys, 0, R.string.menu_importKeys).setIcon(
+ android.R.drawable.ic_menu_add);
+ menu.add(0, Id.menu.option.export_keys, 1, R.string.menu_exportKeys).setIcon(
+ android.R.drawable.ic_menu_save);
+ menu.add(1, Id.menu.option.search, 2, R.string.menu_search).setIcon(
+ android.R.drawable.ic_menu_search);
+ menu.add(1, Id.menu.option.preferences, 3, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(1, Id.menu.option.about, 4, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ menu.add(1, Id.menu.option.scanQRCode, 5, R.string.menu_scanQRCode).setIcon(
+ android.R.drawable.ic_menu_add);
+ return true;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ // TODO: user id? menu.setHeaderTitle("Key");
+ menu.add(0, Id.menu.export, 0, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 1, R.string.menu_deleteKey);
+ menu.add(0, Id.menu.update, 1, R.string.menu_updateKey);
+ menu.add(0, Id.menu.exportToServer, 1, R.string.menu_exportKeyToServer);
+ menu.add(0, Id.menu.signKey, 1, R.string.menu_signKey);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuItem.getMenuInfo();
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type != ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ return super.onContextItemSelected(menuItem);
+ }
+
+ switch (menuItem.getItemId()) {
+ case Id.menu.update: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+ long keyId = 0;
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null && keyRing instanceof PGPPublicKeyRing) {
+ keyId = Apg.getMasterKey((PGPPublicKeyRing) keyRing).getKeyID();
+ }
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent intent = new Intent(this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+
+ return true;
+ }
+
+ case Id.menu.exportToServer: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+
+ Intent intent = new Intent(this, SendKeyActivity.class);
+ intent.setAction(Apg.Intent.EXPORT_KEY_TO_SERVER);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyRingId);
+ startActivityForResult(intent, Id.request.export_to_server);
+
+ return true;
+ }
+
+ case Id.menu.signKey: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+ long keyId = 0;
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null && keyRing instanceof PGPPublicKeyRing) {
+ keyId = Apg.getMasterKey((PGPPublicKeyRing) keyRing).getKeyID();
+ }
+
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent intent = new Intent(this, SignKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivity(intent);
+
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.scanQRCode: {
+ Intent intent = new Intent(this, ImportFromQRCodeActivity.class);
+ intent.setAction(Apg.Intent.IMPORT_FROM_QR_CODE);
+ startActivityForResult(intent, Id.request.import_from_qr_code);
+
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.look_up_key_id: {
+ if (resultCode == RESULT_CANCELED || data == null
+ || data.getStringExtra(Apg.EXTRA_TEXT) == null) {
+ return;
+ }
+
+ Intent intent = new Intent(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ intent.putExtra(Apg.EXTRA_TEXT, data.getStringExtra(Apg.EXTRA_TEXT));
+ handleIntent(intent);
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SecretKeyListActivity.java b/org_apg/src/org/apg/ui/SecretKeyListActivity.java
new file mode 100644
index 000000000..a5d351bc6
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SecretKeyListActivity.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.AskForSecretKeyPassPhrase;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Constants.path;
+import org.apg.Id.dialog;
+import org.apg.Id.menu;
+import org.apg.Id.message;
+import org.apg.Id.type;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView.OnChildClickListener;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+public class SecretKeyListActivity extends KeyListActivity implements OnChildClickListener {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mExportFilename = Constants.path.APP_DIR + "/secexport.asc";
+ mKeyType = Id.type.secret_key;
+ super.onCreate(savedInstanceState);
+ mList.setOnChildClickListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.import_keys, 0, R.string.menu_importKeys)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(0, Id.menu.option.export_keys, 1, R.string.menu_exportKeys)
+ .setIcon(android.R.drawable.ic_menu_save);
+ menu.add(1, Id.menu.option.create, 2, R.string.menu_createKey)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(3, Id.menu.option.search, 3, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ menu.add(3, Id.menu.option.preferences, 4, R.string.menu_preferences)
+ .setIcon(android.R.drawable.ic_menu_preferences);
+ menu.add(3, Id.menu.option.about, 5, R.string.menu_about)
+ .setIcon(android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ createKey();
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ExpandableListView.ExpandableListContextMenuInfo info =
+ (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ // TODO: user id? menu.setHeaderTitle("Key");
+ menu.add(0, Id.menu.edit, 0, R.string.menu_editKey);
+ menu.add(0, Id.menu.export, 1, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 2, R.string.menu_deleteKey);
+ menu.add(0, Id.menu.share, 2, R.string.menu_share);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuItem.getMenuInfo();
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type != ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ return super.onContextItemSelected(menuItem);
+ }
+
+ switch (menuItem.getItemId()) {
+ case Id.menu.edit: {
+ mSelectedItem = groupPosition;
+ checkPassPhraseAndEdit();
+ return true;
+ }
+
+ case Id.menu.share: {
+ mSelectedItem = groupPosition;
+
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ String msg = keyId + "," + Apg.getFingerPrint(keyId);;
+
+ new IntentIntegrator(this).shareText(msg);
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ mSelectedItem = groupPosition;
+ checkPassPhraseAndEdit();
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.pass_phrase: {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ return AskForSecretKeyPassPhrase.createDialog(this, keyId, this);
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void checkPassPhraseAndEdit() {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ String passPhrase = Apg.getCachedPassPhrase(keyId);
+ if (passPhrase == null) {
+ showDialog(Id.dialog.pass_phrase);
+ } else {
+ Apg.setEditPassPhrase(passPhrase);
+ editKey();
+ }
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ Apg.setEditPassPhrase(passPhrase);
+ editKey();
+ }
+
+ private void createKey() {
+ Apg.setEditPassPhrase("");
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ startActivityForResult(intent, Id.message.create_key);
+ }
+
+ private void editKey() {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.message.edit_key);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.message.create_key: // intentionally no break
+ case Id.message.edit_key: {
+ if (resultCode == RESULT_OK) {
+ refreshList();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java b/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java
new file mode 100644
index 000000000..5216e7a3d
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectPublicKeyListActivity extends BaseActivity {
+ protected ListView mList;
+ protected SelectPublicKeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_public_key);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mList = (ListView) findViewById(R.id.list);
+ // needed in Android 1.5, where the XML attribute gets ignored
+ mList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ Button okButton = (Button) findViewById(R.id.btn_ok);
+ okButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ okClicked();
+ }
+ });
+
+ Button cancelButton = (Button) findViewById(R.id.btn_cancel);
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ cancelClicked();
+ }
+ });
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ long selectedKeyIds[] = null;
+ selectedKeyIds = intent.getLongArrayExtra(Apg.EXTRA_SELECTION);
+
+ if (selectedKeyIds == null) {
+ Vector<Long> vector = new Vector<Long>();
+ for (int i = 0; i < mList.getCount(); ++i) {
+ if (mList.isItemChecked(i)) {
+ vector.add(mList.getItemIdAtPosition(i));
+ }
+ }
+ selectedKeyIds = new long[vector.size()];
+ for (int i = 0; i < vector.size(); ++i) {
+ selectedKeyIds[i] = vector.get(i);
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+
+ mListAdapter = new SelectPublicKeyListAdapter(this, mList, searchString, selectedKeyIds);
+ mList.setAdapter(mListAdapter);
+
+ if (selectedKeyIds != null) {
+ for (int i = 0; i < mListAdapter.getCount(); ++i) {
+ long keyId = mListAdapter.getItemId(i);
+ for (int j = 0; j < selectedKeyIds.length; ++j) {
+ if (keyId == selectedKeyIds[j]) {
+ mList.setItemChecked(i, true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<Long> keys = new Vector<Long>();
+ Vector<String> userIds = new Vector<String>();
+ for (int i = 0; i < mList.getCount(); ++i) {
+ if (mList.isItemChecked(i)) {
+ keys.add(mList.getItemIdAtPosition(i));
+ userIds.add((String) mList.getItemAtPosition(i));
+ }
+ }
+ long selectedKeyIds[] = new long[keys.size()];
+ for (int i = 0; i < keys.size(); ++i) {
+ selectedKeyIds[i] = keys.get(i);
+ }
+ String userIdArray[] = new String[0];
+ data.putExtra(Apg.EXTRA_SELECTION, selectedKeyIds);
+ data.putExtra(Apg.EXTRA_USER_IDS, userIds.toArray(userIdArray));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.search, 0, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java b/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java
new file mode 100644
index 000000000..b2f49f74a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import java.util.Date;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.database;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectPublicKeyListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected ListView mParent;
+ protected SQLiteDatabase mDatabase;
+ protected Cursor mCursor;
+ protected String mSearchString;
+ protected Activity mActivity;
+
+ public SelectPublicKeyListAdapter(Activity activity, ListView parent,
+ String searchString, long selectedKeyIds[]) {
+ mSearchString = searchString;
+
+ mActivity = activity;
+ mParent = parent;
+ mDatabase = Apg.getDatabase().db();
+ mInflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ long now = new Date().getTime() / 1000;
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ String inIdList = null;
+
+ if (selectedKeyIds != null && selectedKeyIds.length > 0) {
+ inIdList = KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < selectedKeyIds.length; ++i) {
+ if (i != 0) {
+ inIdList += ", ";
+ }
+ inIdList += DatabaseUtils.sqlEscapeString("" + selectedKeyIds[i]);
+ }
+ inIdList += ")";
+ }
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " +
+ UserIds.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + UserIds.KEY_ID + " = " +
+ Keys.TABLE_NAME + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere("))");
+
+ if (inIdList != null) {
+ qb.appendWhere(" OR (" + inIdList + ")");
+ }
+ }
+
+ String orderBy = UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC";
+ if (inIdList != null) {
+ orderBy = inIdList + " DESC, " + orderBy;
+ }
+
+ mCursor = qb.query(mDatabase,
+ new String[] {
+ KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_ENCRYPT + " = '1')", // 3
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_ENCRYPT + " = '1' AND " +
+ "tmp." + Keys.CREATION + " <= '" + now + "' AND " +
+ "(tmp." + Keys.EXPIRY + " IS NULL OR " +
+ "tmp." + Keys.EXPIRY + " >= '" + now + "'))", // 4
+ },
+ KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?",
+ new String[] { "" + Id.database.type_public },
+ null, null, orderBy);
+
+ activity.startManagingCursor(mCursor);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ mActivity.stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(4) > 0; // valid CAN_ENCRYPT
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getItem(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(2); // USER_ID
+ }
+
+ public long getItemId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(position);
+
+ View view = mInflater.inflate(R.layout.select_public_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText(R.string.unknownStatus);
+
+ String userId = mCursor.getString(2); // USER_ID
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ long masterKeyId = mCursor.getLong(1); // MASTER_KEY_ID
+ keyId.setText(Apg.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (enabled) {
+ status.setText(R.string.canEncrypt);
+ } else {
+ if (mCursor.getInt(3) > 0) {
+ // has some CAN_ENCRYPT keys, but col(4) = 0, so must be revoked or expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ status.setText(status.getText() + " ");
+
+ CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
+
+ if (!enabled) {
+ mParent.setItemChecked(position, false);
+ }
+
+ selected.setChecked(mParent.isItemChecked(position));
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ selected.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java b/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java
new file mode 100644
index 000000000..191a0ecc7
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectSecretKeyListActivity extends BaseActivity {
+ protected ListView mList;
+ protected SelectSecretKeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ protected long mSelectedKeyId = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ setContentView(R.layout.select_secret_key);
+
+ mList = (ListView) findViewById(R.id.list);
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ Intent data = new Intent();
+ data.putExtra(Apg.EXTRA_KEY_ID, id);
+ data.putExtra(Apg.EXTRA_USER_ID, (String)mList.getItemAtPosition(position));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+
+ mListAdapter = new SelectSecretKeyListAdapter(this, mList, searchString);
+ mList.setAdapter(mListAdapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.search, 0, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java b/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java
new file mode 100644
index 000000000..1a7734245
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java
@@ -0,0 +1,176 @@
+package org.apg.ui;
+
+import java.util.Date;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.database;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectSecretKeyListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected ListView mParent;
+ protected SQLiteDatabase mDatabase;
+ protected Cursor mCursor;
+ protected String mSearchString;
+ protected Activity mActivity;
+
+ public SelectSecretKeyListAdapter(Activity activity, ListView parent, String searchString) {
+ mSearchString = searchString;
+
+ mActivity = activity;
+ mParent = parent;
+ mDatabase = Apg.getDatabase().db();
+ mInflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ long now = new Date().getTime() / 1000;
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " +
+ UserIds.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + UserIds.KEY_ID + " = " +
+ Keys.TABLE_NAME + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere(")");
+ }
+
+ mCursor = qb.query(mDatabase,
+ new String[] {
+ KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_SIGN + " = '1')", // 3,
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_SIGN + " = '1' AND " +
+ "tmp." + Keys.CREATION + " <= '" + now + "' AND " +
+ "(tmp." + Keys.EXPIRY + " IS NULL OR " +
+ "tmp." + Keys.EXPIRY + " >= '" + now + "'))", // 4
+ },
+ KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?",
+ new String[] { "" + Id.database.type_secret },
+ null, null, UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC");
+
+ activity.startManagingCursor(mCursor);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ mActivity.stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(4) > 0; // valid CAN_SIGN
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getItem(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(2); // USER_ID
+ }
+
+ public long getItemId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(position);
+
+ View view = mInflater.inflate(R.layout.select_secret_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText(R.string.unknownStatus);
+
+ String userId = mCursor.getString(2); // USER_ID
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ long masterKeyId = mCursor.getLong(1); // MASTER_KEY_ID
+ keyId.setText(Apg.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (enabled) {
+ status.setText(R.string.canSign);
+ } else {
+ if (mCursor.getInt(3) > 0) {
+ // has some CAN_SIGN keys, but col(4) = 0, so must be revoked or expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ status.setText(status.getText() + " ");
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+} \ No newline at end of file
diff --git a/org_apg/src/org/apg/ui/SendKeyActivity.java b/org_apg/src/org/apg/ui/SendKeyActivity.java
new file mode 100644
index 000000000..c44e87469
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SendKeyActivity.java
@@ -0,0 +1,100 @@
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.Constants.extras;
+import org.apg.Id.message;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.os.Bundle;
+import android.os.Message;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --send-key activity
+ *
+ * Sends the selected public key to a key server
+ */
+public class SendKeyActivity extends BaseActivity {
+
+ private Button export;
+ private Spinner keyServer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_export_layout);
+
+ export = (Button) findViewById(R.id.btn_export_to_server);
+ keyServer = (Spinner) findViewById(R.id.keyServer);
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ keyServer.setSelection(0);
+ } else {
+ export.setEnabled(false);
+ }
+
+ export.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startThread();
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
+
+ int keyRingId = getIntent().getIntExtra(Apg.EXTRA_KEY_ID, -1);
+
+ PGPKeyRing keyring = Apg.getKeyRing(keyRingId);
+ if (keyring != null && keyring instanceof PGPPublicKeyRing) {
+ boolean uploaded = Apg.uploadKeyRingToServer(server, (PGPPublicKeyRing) keyring);
+ if (!uploaded) {
+ error = "Unable to export key to selected server";
+ }
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.export_done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySendSuccess, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SignKeyActivity.java b/org_apg/src/org/apg/ui/SignKeyActivity.java
new file mode 100644
index 000000000..ab145c921
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SignKeyActivity.java
@@ -0,0 +1,294 @@
+package org.apg.ui;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Iterator;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.Constants.extras;
+import org.apg.Id.dialog;
+import org.apg.Id.message;
+import org.apg.Id.request;
+import org.apg.Id.return_value;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --sign-key
+ *
+ * signs the specified public key with the specified secret master key
+ */
+public class SignKeyActivity extends BaseActivity {
+ private static final String TAG = "SignKeyActivity";
+
+ private long pubKeyId = 0;
+ private long masterKeyId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check we havent already signed it
+ setContentView(R.layout.sign_key_layout);
+
+ final Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+
+ final CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (!sendKey.isChecked()) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+
+ sendKey.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (!isChecked) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+ }
+ });
+
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(false); // disabled until the user selects a key to sign with
+ sign.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (pubKeyId != 0) {
+ initiateSigning();
+ }
+ }
+ });
+
+ pubKeyId = getIntent().getLongExtra(Apg.EXTRA_KEY_ID, 0);
+ if (pubKeyId == 0) {
+ finish(); // nothing to do if we dont know what key to sign
+ } else {
+ // kick off the SecretKey selection activity so the user chooses which key to sign with first
+ Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+ }
+
+ /**
+ * handles the UI bits of the signing process on the UI thread
+ */
+ private void initiateSigning() {
+ PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
+ if (pubring != null) {
+ // if we have already signed this key, dont bother doing it again
+ boolean alreadySigned = false;
+
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> itr = pubring.getPublicKey(pubKeyId).getSignatures();
+ while (itr.hasNext()) {
+ PGPSignature sig = itr.next();
+ if (sig.getKeyID() == masterKeyId) {
+ alreadySigned = true;
+ break;
+ }
+ }
+
+ if (!alreadySigned) {
+ /*
+ * get the user's passphrase for this key (if required)
+ */
+ String passphrase = Apg.getCachedPassPhrase(masterKeyId);
+ if (passphrase == null) {
+ showDialog(Id.dialog.pass_phrase);
+ return; // bail out; need to wait until the user has entered the passphrase before trying again
+ } else {
+ startSigning();
+ }
+ } else {
+ final Bundle status = new Bundle();
+ Message msg = new Message();
+
+ status.putString(Apg.EXTRA_ERROR, "Key has already been signed");
+
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ setResult(Id.return_value.error);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public long getSecretKeyId() {
+ return masterKeyId;
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ startSigning();
+ }
+
+ /**
+ * kicks off the actual signing process on a background thread
+ */
+ private void startSigning() {
+ showDialog(Id.dialog.signing);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ final Bundle status = new Bundle();
+ Message msg = new Message();
+
+ try {
+ String passphrase = Apg.getCachedPassPhrase(masterKeyId);
+ if (passphrase == null || passphrase.length() <= 0) {
+ status.putString(Apg.EXTRA_ERROR, "Unable to obtain passphrase");
+ } else {
+ PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
+
+ /*
+ * sign the incoming key
+ */
+ PGPSecretKey secretKey = Apg.getSecretKey(masterKeyId);
+ PGPPrivateKey signingKey = secretKey.extractPrivateKey(passphrase.toCharArray(), BouncyCastleProvider.PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(secretKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256, BouncyCastleProvider.PROVIDER_NAME);
+ sGen.initSign(PGPSignature.DIRECT_KEY, signingKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+
+ PGPSignatureSubpacketVector packetVector = spGen.generate();
+ sGen.setHashedSubpackets(packetVector);
+
+ PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId), sGen.generate());
+ pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
+
+ // check if we need to send the key to the server or not
+ CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (sendKey.isChecked()) {
+ Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
+
+ /*
+ * upload the newly signed key to the key server
+ */
+
+ Apg.uploadKeyRingToServer(server, pubring);
+ }
+
+ // store the signed key in our local cache
+ int retval = Apg.storeKeyRingInCache(pubring);
+ if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
+ status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
+ }
+ }
+ } catch (PGPException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (NoSuchProviderException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (SignatureException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ }
+
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ if (status.containsKey(Apg.EXTRA_ERROR)) {
+ setResult(Id.return_value.error);
+ } else {
+ setResult(Id.return_value.ok);
+ }
+
+ finish();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ masterKeyId = data.getLongExtra(Apg.EXTRA_KEY_ID, 0);
+
+ // re-enable the sign button so the user can initiate the sign process
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(true);
+ }
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.signing);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/Editor.java b/org_apg/src/org/apg/ui/widget/Editor.java
new file mode 100644
index 000000000..be95ad656
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/Editor.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+public interface Editor {
+ public interface EditorListener {
+ public void onDeleted(Editor editor);
+ }
+
+ public void setEditorListener(EditorListener listener);
+}
diff --git a/org_apg/src/org/apg/ui/widget/IntegerListPreference.java b/org_apg/src/org/apg/ui/widget/IntegerListPreference.java
new file mode 100644
index 000000000..fa411a786
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/IntegerListPreference.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apg.ui.widget;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+/**
+ * A list preference which persists its values as integers instead of strings.
+ * Code reading the values should use
+ * {@link android.content.SharedPreferences#getInt}.
+ * When using XML-declared arrays for entry values, the arrays should be regular
+ * string arrays containing valid integer values.
+ *
+ * @author Rodrigo Damazio
+ */
+public class IntegerListPreference extends ListPreference {
+
+ public IntegerListPreference(Context context) {
+ super(context);
+
+ verifyEntryValues(null);
+ }
+
+ public IntegerListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ verifyEntryValues(null);
+ }
+
+ @Override
+ public void setEntryValues(CharSequence[] entryValues) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValues);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ public void setEntryValues(int entryValuesResId) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValuesResId);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ protected String getPersistedString(String defaultReturnValue) {
+ // During initial load, there's no known default value
+ int defaultIntegerValue = Integer.MIN_VALUE;
+ if (defaultReturnValue != null) {
+ defaultIntegerValue = Integer.parseInt(defaultReturnValue);
+ }
+
+ // When the list preference asks us to read a string, instead read an
+ // integer.
+ int value = getPersistedInt(defaultIntegerValue);
+ return Integer.toString(value);
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ // When asked to save a string, instead save an integer
+ return persistInt(Integer.parseInt(value));
+ }
+
+ private void verifyEntryValues(CharSequence[] oldValues) {
+ CharSequence[] entryValues = getEntryValues();
+ if (entryValues == null) {
+ return;
+ }
+
+ for (CharSequence entryValue : entryValues) {
+ try {
+ Integer.parseInt(entryValue.toString());
+ } catch (NumberFormatException nfe) {
+ super.setEntryValues(oldValues);
+ throw nfe;
+ }
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/KeyEditor.java b/org_apg/src/org/apg/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..ef98f794a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/KeyEditor.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.util.Choice;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.apg.R;
+
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Vector;
+
+public class KeyEditor extends LinearLayout implements Editor, OnClickListener {
+ private PGPSecretKey mKey;
+
+ private EditorListener mEditorListener = null;
+
+ private boolean mIsMasterKey;
+ ImageButton mDeleteButton;
+ TextView mAlgorithm;
+ TextView mKeyId;
+ Spinner mUsage;
+ TextView mCreationDate;
+ Button mExpiryDateButton;
+ GregorianCalendar mExpiryDate;
+
+ private DatePickerDialog.OnDateSetListener mExpiryDateSetListener =
+ new DatePickerDialog.OnDateSetListener() {
+ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+ GregorianCalendar date = new GregorianCalendar(year, monthOfYear, dayOfMonth);
+ setExpiryDate(date);
+ }
+ };
+
+ public KeyEditor(Context context) {
+ super(context);
+ }
+
+ public KeyEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAlgorithm = (TextView) findViewById(R.id.algorithm);
+ mKeyId = (TextView) findViewById(R.id.keyId);
+ mCreationDate = (TextView) findViewById(R.id.creation);
+ mExpiryDateButton = (Button) findViewById(R.id.expiry);
+ mUsage = (Spinner) findViewById(R.id.usage);
+ Choice choices[] = {
+ new Choice(Id.choice.usage.sign_only,
+ getResources().getString(R.string.choice_signOnly)),
+ new Choice(Id.choice.usage.encrypt_only,
+ getResources().getString(R.string.choice_encryptOnly)),
+ new Choice(Id.choice.usage.sign_and_encrypt,
+ getResources().getString(R.string.choice_signAndEncrypt)),
+ };
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ setExpiryDate(null);
+
+ mExpiryDateButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ GregorianCalendar date = mExpiryDate;
+ if (date == null) {
+ date = new GregorianCalendar();
+ }
+
+ DatePickerDialog dialog =
+ new DatePickerDialog(getContext(), mExpiryDateSetListener,
+ date.get(Calendar.YEAR),
+ date.get(Calendar.MONTH),
+ date.get(Calendar.DAY_OF_MONTH));
+ dialog.setCancelable(true);
+ dialog.setButton(Dialog.BUTTON_NEGATIVE,
+ getContext().getString(R.string.btn_noDate),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setExpiryDate(null);
+ }
+ });
+ dialog.show();
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(PGPSecretKey key, boolean isMasterKey) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(Apg.getAlgorithmInfo(key));
+ String keyId1Str = Apg.getSmallFingerPrint(key.getKeyID());
+ String keyId2Str = Apg.getSmallFingerPrint(key.getKeyID() >> 32);
+ mKeyId.setText(keyId1Str + " " + keyId2Str);
+
+ Vector<Choice> choices = new Vector<Choice>();
+ boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT);
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_only,
+ getResources().getString(R.string.choice_signOnly)));
+ }
+ if (!mIsMasterKey) {
+ choices.add(new Choice(Id.choice.usage.encrypt_only,
+ getResources().getString(R.string.choice_encryptOnly)));
+ }
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_and_encrypt,
+ getResources().getString(R.string.choice_signAndEncrypt)));
+ }
+
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ int selectId = 0;
+ if (Apg.isEncryptionKey(key)) {
+ if (Apg.isSigningKey(key)) {
+ selectId = Id.choice.usage.sign_and_encrypt;
+ } else {
+ selectId = Id.choice.usage.encrypt_only;
+ }
+ } else {
+ selectId = Id.choice.usage.sign_only;
+ }
+
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == selectId) {
+ mUsage.setSelection(i);
+ break;
+ }
+ }
+
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTime(Apg.getCreationDate(key));
+ mCreationDate.setText(DateFormat.getDateInstance().format(cal.getTime()));
+ cal = new GregorianCalendar();
+ Date date = Apg.getExpiryDate(key);
+ if (date == null) {
+ setExpiryDate(null);
+ } else {
+ cal.setTime(Apg.getExpiryDate(key));
+ setExpiryDate(cal);
+ }
+ }
+
+ public PGPSecretKey getValue() {
+ return mKey;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ private void setExpiryDate(GregorianCalendar date) {
+ mExpiryDate = date;
+ if (date == null) {
+ mExpiryDateButton.setText(R.string.none);
+ } else {
+ mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime()));
+ }
+ }
+
+ public GregorianCalendar getExpiryDate() {
+ return mExpiryDate;
+ }
+
+ public int getUsage() {
+ return ((Choice) mUsage.getSelectedItem()).getId();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/KeyServerEditor.java b/org_apg/src/org/apg/ui/widget/KeyServerEditor.java
new file mode 100644
index 000000000..3d8634c76
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/KeyServerEditor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+import org.apg.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ ImageButton mDeleteButton;
+ TextView mServer;
+
+ public KeyServerEditor(Context context) {
+ super(context);
+ }
+
+ public KeyServerEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mServer = (TextView) findViewById(R.id.server);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String value) {
+ mServer.setText(value);
+ }
+
+ public String getValue() {
+ return mServer.getText().toString().trim();
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/SectionView.java b/org_apg/src/org/apg/ui/widget/SectionView.java
new file mode 100644
index 000000000..220699124
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/SectionView.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.ui.widget.Editor.EditorListener;
+import org.apg.util.Choice;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Vector;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener, Runnable {
+ private LayoutInflater mInflater;
+ private View mAdd;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+
+ private Choice mNewKeyAlgorithmChoice;
+ private int mNewKeySize;
+
+ volatile private PGPSecretKey mNewKey;
+ private ProgressDialog mProgressDialog;
+ private Thread mRunningThread = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ boolean closeProgressDialog = data.getBoolean("closeProgressDialog");
+ if (closeProgressDialog) {
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(getContext(),
+ getContext().getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ boolean gotNewKey = data.getBoolean("gotNewKey");
+ if (gotNewKey) {
+ KeyEditor view =
+ (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item,
+ mEditors, false);
+ view.setEditorListener(SectionView.this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(mNewKey, isMasterKey);
+ mEditors.addView(view);
+ SectionView.this.updateEditorsVisible();
+ }
+ }
+ }
+ };
+
+ public SectionView(Context context) {
+ super(context);
+ }
+
+ public SectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ViewGroup getEditors() {
+ return mEditors;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ switch (type) {
+ case Id.type.user_id: {
+ mTitle.setText(R.string.section_userIds);
+ break;
+ }
+
+ case Id.type.key: {
+ mTitle.setText(R.string.section_keys);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onFinishInflate() {
+ mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAdd = findViewById(R.id.header);
+ mAdd.setOnClickListener(this);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mTitle = (TextView) findViewById(R.id.title);
+
+ updateEditorsVisible();
+ super.onFinishInflate();
+ }
+
+ /** {@inheritDoc} */
+ public void onDeleted(Editor editor) {
+ this.updateEditorsVisible();
+ }
+
+ protected void updateEditorsVisible() {
+ final boolean hasChildren = mEditors.getChildCount() > 0;
+ mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ switch (mType) {
+ case Id.type.user_id: {
+ UserIdEditor view =
+ (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item,
+ mEditors, false);
+ view.setEditorListener(this);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ break;
+ }
+
+ case Id.type.key: {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+
+ View view = mInflater.inflate(R.layout.create_key, null);
+ dialog.setView(view);
+ dialog.setTitle(R.string.title_createKey);
+ dialog.setMessage(R.string.keyCreationElGamalInfo);
+
+ boolean wouldBeMasterKey = (mEditors.getChildCount() == 0);
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.algorithm);
+ Vector<Choice> choices = new Vector<Choice>();
+ choices.add(new Choice(Id.choice.algorithm.dsa,
+ getResources().getString(R.string.dsa)));
+ if (!wouldBeMasterKey) {
+ choices.add(new Choice(Id.choice.algorithm.elgamal,
+ getResources().getString(R.string.elgamal)));
+ }
+
+ choices.add(new Choice(Id.choice.algorithm.rsa,
+ getResources().getString(R.string.rsa)));
+
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item,
+ choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ algorithm.setAdapter(adapter);
+ // make RSA the default
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == Id.choice.algorithm.rsa) {
+ algorithm.setSelection(i);
+ break;
+ }
+ }
+
+ final EditText keySize = (EditText) view.findViewById(R.id.size);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ try {
+ mNewKeySize = Integer.parseInt("" + keySize.getText());
+ } catch (NumberFormatException e) {
+ mNewKeySize = 0;
+ }
+
+ mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem();
+ createKey();
+ }
+ });
+
+ dialog.setCancelable(true);
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ }
+ });
+
+ dialog.create().show();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ this.updateEditorsVisible();
+ }
+
+ public void setUserIds(Vector<String> list) {
+ if (mType != Id.type.user_id) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+ for (String userId : list) {
+ UserIdEditor view =
+ (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(userId);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ public void setKeys(Vector<PGPSecretKey> list) {
+ if (mType != Id.type.key) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+ for (PGPSecretKey key : list) {
+ KeyEditor view =
+ (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, mEditors, false);
+ view.setEditorListener(this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(key, isMasterKey);
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ private void createKey() {
+ mProgressDialog = new ProgressDialog(getContext());
+ mProgressDialog.setMessage(getContext().getString(R.string.progress_generating));
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.show();
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ try {
+ PGPSecretKey masterKey = null;
+ String passPhrase;
+ if (mEditors.getChildCount() > 0) {
+ masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue();
+ passPhrase = Apg.getCachedPassPhrase(masterKey.getKeyID());
+ } else {
+ passPhrase = "";
+ }
+ mNewKey = Apg.createKey(getContext(),
+ mNewKeyAlgorithmChoice.getId(),
+ mNewKeySize, passPhrase,
+ masterKey);
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (InvalidParameterException e) {
+ error = "" + e;
+ } catch (InvalidAlgorithmParameterException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ Message message = new Message();
+ Bundle data = new Bundle();
+ data.putBoolean("closeProgressDialog", true);
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ } else {
+ data.putBoolean("gotNewKey", true);
+ }
+ message.setData(data);
+ mHandler.sendMessage(message);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/UserIdEditor.java b/org_apg/src/org/apg/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..b154803cf
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/UserIdEditor.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apg.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+
+public class UserIdEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ private ImageButton mDeleteButton;
+ private RadioButton mIsMainUserId;
+ private EditText mName;
+ private EditText mEmail;
+ private EditText mComment;
+
+ private static final Pattern EMAIL_PATTERN =
+ Pattern.compile("^([a-zA-Z0-9_.-])+@([a-zA-Z0-9_.-])+[.]([a-zA-Z])+([a-zA-Z])+",
+ Pattern.CASE_INSENSITIVE);
+
+ public static class NoNameException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public NoNameException(String message) {
+ super(message);
+ }
+ }
+
+ public static class NoEmailException extends Exception {
+ static final long serialVersionUID = 0xf812773344L;
+
+ public NoEmailException(String message) {
+ super(message);
+ }
+ }
+
+ public static class InvalidEmailException extends Exception {
+ static final long serialVersionUID = 0xf812773345L;
+
+ public InvalidEmailException(String message) {
+ super(message);
+ }
+ }
+
+ public UserIdEditor(Context context) {
+ super(context);
+ }
+
+ public UserIdEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+ mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId);
+ mIsMainUserId.setOnClickListener(this);
+
+ mName = (EditText) findViewById(R.id.name);
+ mEmail = (EditText) findViewById(R.id.email);
+ mComment = (EditText) findViewById(R.id.comment);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String userId) {
+ mName.setText("");
+ mComment.setText("");
+ mEmail.setText("");
+
+ Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$");
+ Matcher matcher = withComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mComment.setText(matcher.group(2));
+ mEmail.setText(matcher.group(3));
+ return;
+ }
+
+ Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$");
+ matcher = withoutComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mEmail.setText(matcher.group(2));
+ return;
+ }
+ }
+
+ public String getValue() throws NoNameException, NoEmailException, InvalidEmailException {
+ String name = ("" + mName.getText()).trim();
+ String email = ("" + mEmail.getText()).trim();
+ String comment = ("" + mComment.getText()).trim();
+
+ if (email.length() > 0) {
+ Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
+ if (!emailMatcher.matches()) {
+ throw new InvalidEmailException(
+ getContext().getString(R.string.error_invalidEmail, email));
+ }
+ }
+
+ String userId = name;
+ if (comment.length() > 0) {
+ userId += " (" + comment + ")";
+ }
+ if (email.length() > 0) {
+ userId += " <" + email + ">";
+ }
+
+ if (userId.equals("")) {
+ // ok, empty one...
+ return userId;
+ }
+
+ // otherwise make sure that name and email exist
+ if (name.equals("")) {
+ throw new NoNameException("need a name");
+ }
+
+ if (email.equals("")) {
+ throw new NoEmailException("need an email");
+ }
+
+ return userId;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ boolean wasMainUserId = mIsMainUserId.isChecked();
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ if (wasMainUserId && parent.getChildCount() > 0) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(0);
+ editor.setIsMainUserId(true);
+ }
+ } else if (v == mIsMainUserId) {
+ for (int i = 0; i < parent.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(i);
+ if (editor == this) {
+ editor.setIsMainUserId(true);
+ } else {
+ editor.setIsMainUserId(false);
+ }
+ }
+ }
+ }
+
+ public void setIsMainUserId(boolean value) {
+ mIsMainUserId.setChecked(value);
+ }
+
+ public boolean isMainUserId() {
+ return mIsMainUserId.isChecked();
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/org_apg/src/org/apg/util/ApgCon.java b/org_apg/src/org/apg/util/ApgCon.java
new file mode 100644
index 000000000..73ee66ad9
--- /dev/null
+++ b/org_apg/src/org/apg/util/ApgCon.java
@@ -0,0 +1,836 @@
+/*
+ * Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
+ *
+ * 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.apg.util;
+
+import org.apg.util.ApgConInterface.OnCallFinishListener;
+import org.apg.IApgService;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+
+/**
+ * A APG-AIDL-Wrapper
+ *
+ * <p>
+ * This class can be used by other projects to simplify connecting to the
+ * APG-AIDL-Service. Kind of wrapper of for AIDL.
+ * </p>
+ *
+ * <p>
+ * It is not used in this project.
+ * </p>
+ *
+ * @author Markus Doits <markus.doits@googlemail.com>
+ * @version 1.1rc1
+ *
+ */
+public class ApgCon {
+ private static final boolean LOCAL_LOGV = true;
+ private static final boolean LOCAL_LOGD = true;
+
+ private final static String TAG = "ApgCon";
+ private final static int API_VERSION = 2; // aidl api-version it expects
+ private final static String BLOB_URI = "content://org.thialfihar.android.apg.provider.apgserviceblobprovider";
+
+ /**
+ * How many seconds to wait for a connection to AGP when connecting.
+ * Being unsuccessful for this number of seconds, a connection
+ * is assumed to be failed.
+ */
+ public int secondsToWaitForConnection = 15;
+
+ private class CallAsync extends AsyncTask<String, Void, Void> {
+
+ @Override
+ protected Void doInBackground(String... arg) {
+ if( LOCAL_LOGD ) Log.d(TAG, "Async execution starting");
+ call(arg[0]);
+ return null;
+ }
+
+ protected void onPostExecute(Void res) {
+ if( LOCAL_LOGD ) Log.d(TAG, "Async execution finished");
+ mAsyncRunning = false;
+
+ }
+
+ }
+
+ private final Context mContext;
+ private final error mConnectionStatus;
+ private boolean mAsyncRunning = false;
+ private OnCallFinishListener mOnCallFinishListener;
+
+ private final Bundle mResult = new Bundle();
+ private final Bundle mArgs = new Bundle();
+ private final ArrayList<String> mErrorList = new ArrayList<String>();
+ private final ArrayList<String> mWarningList = new ArrayList<String>();
+
+ /** Remote service for decrypting and encrypting data */
+ private IApgService mApgService = null;
+
+ /** Set apgService accordingly to connection status */
+ private ServiceConnection mApgConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if( LOCAL_LOGD ) Log.d(TAG, "IApgService bound to apgService");
+ mApgService = IApgService.Stub.asInterface(service);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ if( LOCAL_LOGD ) Log.d(TAG, "IApgService disconnected");
+ mApgService = null;
+ }
+ };
+
+ /**
+ * Different types of local errors
+ */
+ public static enum error {
+ /**
+ * no error
+ */
+ NO_ERROR,
+ /**
+ * generic error
+ */
+ GENERIC,
+ /**
+ * connection to apg service not possible
+ */
+ CANNOT_BIND_TO_APG,
+ /**
+ * function to call not provided
+ */
+ CALL_MISSING,
+ /**
+ * apg service does not know what to do
+ */
+ CALL_NOT_KNOWN,
+ /**
+ * could not find APG being installed
+ */
+ APG_NOT_FOUND,
+ /**
+ * found APG but without AIDL interface
+ */
+ APG_AIDL_MISSING,
+ /**
+ * found APG but with wrong API
+ */
+ APG_API_MISSMATCH
+ }
+
+ private static enum ret {
+ ERROR, // returned from AIDL
+ RESULT, // returned from AIDL
+ WARNINGS, // mixed AIDL and LOCAL
+ ERRORS, // mixed AIDL and LOCAL
+ }
+
+ /**
+ * Constructor
+ *
+ * <p>
+ * Creates a new ApgCon object and searches for the right APG version on
+ * initialization. If not found, errors are printed to the error log.
+ * </p>
+ *
+ * @param ctx
+ * the running context
+ */
+ public ApgCon(Context ctx) {
+ if( LOCAL_LOGV ) Log.v(TAG, "EncryptionService created");
+ mContext = ctx;
+
+ error tmpError = null;
+ try {
+ if( LOCAL_LOGV ) Log.v(TAG, "Searching for the right APG version");
+ ServiceInfo apgServices[] = ctx.getPackageManager().getPackageInfo("org.thialfihar.android.apg",
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA).services;
+ if (apgServices == null) {
+ Log.e(TAG, "Could not fetch services");
+ tmpError = error.GENERIC;
+ } else {
+ boolean apgServiceFound = false;
+ for (ServiceInfo inf : apgServices) {
+ if( LOCAL_LOGV ) Log.v(TAG, "Found service of APG: " + inf.name);
+ if (inf.name.equals("org.thialfihar.android.apg.ApgService")) {
+ apgServiceFound = true;
+ if (inf.metaData == null) {
+ Log.w(TAG, "Could not determine ApgService API");
+ Log.w(TAG, "This probably won't work!");
+ mWarningList.add("(LOCAL) Could not determine ApgService API");
+ tmpError = error.APG_API_MISSMATCH;
+ } else if (inf.metaData.getInt("api_version") != API_VERSION) {
+ Log.w(TAG, "Found ApgService API version " + inf.metaData.getInt("api_version") + " but exspected " + API_VERSION);
+ Log.w(TAG, "This probably won't work!");
+ mWarningList.add("(LOCAL) Found ApgService API version " + inf.metaData.getInt("api_version") + " but exspected " + API_VERSION);
+ tmpError = error.APG_API_MISSMATCH;
+ } else {
+ if( LOCAL_LOGV ) Log.v(TAG, "Found api_version " + API_VERSION + ", everything should work");
+ tmpError = error.NO_ERROR;
+ }
+ }
+ }
+
+ if (!apgServiceFound) {
+ Log.e(TAG, "Could not find APG with AIDL interface, this probably won't work");
+ mErrorList.add("(LOCAL) Could not find APG with AIDL interface, this probably won't work");
+ mResult.putInt(ret.ERROR.name(), error.APG_AIDL_MISSING.ordinal());
+ tmpError = error.APG_NOT_FOUND;
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Could not find APG, is it installed?", e);
+ mErrorList.add("(LOCAL) Could not find APG, is it installed?");
+ mResult.putInt(ret.ERROR.name(), error.APG_NOT_FOUND.ordinal());
+ tmpError = error.APG_NOT_FOUND;
+ }
+
+ mConnectionStatus = tmpError;
+
+ }
+
+ /** try to connect to the apg service */
+ private boolean connect() {
+ if( LOCAL_LOGV ) Log.v(TAG, "trying to bind the apgService to context");
+
+ if (mApgService != null) {
+ if( LOCAL_LOGV ) Log.v(TAG, "allready connected");
+ return true;
+ }
+
+ try {
+ mContext.bindService(new Intent(IApgService.class.getName()), mApgConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception e) {
+ Log.e(TAG, "could not bind APG service", e);
+ return false;
+ }
+
+ int waitCount = 0;
+ while (mApgService == null && waitCount++ < secondsToWaitForConnection) {
+ if( LOCAL_LOGV ) Log.v(TAG, "sleeping 1 second to wait for apg");
+ android.os.SystemClock.sleep(1000);
+ }
+
+ if (waitCount >= secondsToWaitForConnection) {
+ if( LOCAL_LOGV ) Log.v(TAG, "slept waiting for nothing!");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnects ApgCon from Apg
+ *
+ * <p>
+ * This should be called whenever all work with APG is done (e.g. everything
+ * you wanted to encrypt is encrypted), since connections with AIDL should
+ * not be upheld indefinitely.
+ * <p>
+ *
+ * <p>
+ * Also, if you destroy you end using your ApgCon-instance, this must be
+ * called or else the connection to APG is leaked
+ * </p>
+ */
+ public void disconnect() {
+ if( LOCAL_LOGV ) Log.v(TAG, "disconnecting apgService");
+ if (mApgService != null) {
+ mContext.unbindService(mApgConnection);
+ mApgService = null;
+ }
+ }
+
+ private boolean initialize() {
+ if (mApgService == null) {
+ if (!connect()) {
+ if( LOCAL_LOGV ) Log.v(TAG, "connection to apg service failed");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calls a function from APG's AIDL-interface
+ *
+ * <p>
+ * After you have set up everything with {@link #setArg(String, String)}
+ * (and variants), you can call a function of the AIDL-interface. This
+ * will:
+ * <ul>
+ * <li>start connection to the remote interface (if not already connected)</li>
+ * <li>call the function passed with all parameters synchronously</li>
+ * <li>set up everything to retrieve the result and/or warnings/errors</li>
+ * <li>call the callback if provided
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * Note your thread will be blocked during execution - if you want to call
+ * the function asynchronously, see {@link #callAsync(String)}.
+ * </p>
+ *
+ * @param function
+ * a remote function to call
+ * @return true, if call successful (= no errors), else false
+ *
+ * @see #callAsync(String)
+ * @see #setArg(String, String)
+ * @see #setOnCallFinishListener(OnCallFinishListener)
+ */
+ public boolean call(String function) {
+ boolean success = this.call(function, mArgs, mResult);
+ if (mOnCallFinishListener != null) {
+ try {
+ if( LOCAL_LOGD ) Log.d(TAG, "About to execute callback");
+ mOnCallFinishListener.onCallFinish(mResult);
+ if( LOCAL_LOGD ) Log.d(TAG, "Callback executed");
+ } catch (Exception e) {
+ Log.w(TAG, "Exception on callback: (" + e.getClass() + ") " + e.getMessage(), e);
+ mWarningList.add("(LOCAL) Could not execute callback (" + e.getClass() + "): " + e.getMessage());
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Calls a function of remote interface asynchronously
+ *
+ * <p>
+ * This does exactly the same as {@link #call(String)}, but asynchronously.
+ * While connection to APG and work are done in background, your thread can
+ * go on executing.
+ * <p>
+ *
+ * <p>
+ * To see whether the task is finished, you have two possibilities:
+ * <ul>
+ * <li>In your thread, poll {@link #isRunning()}</li>
+ * <li>Supply a callback with {@link #setOnCallFinishListener(OnCallFinishListener)}</li>
+ * </ul>
+ * </p>
+ *
+ * @param function
+ * a remote function to call
+ *
+ * @see #call(String)
+ * @see #isRunning()
+ * @see #setOnCallFinishListener(OnCallFinishListener)
+ */
+ public void callAsync(String function) {
+ mAsyncRunning = true;
+ new CallAsync().execute(function);
+ }
+
+ private boolean call(String function, Bundle pArgs, Bundle pReturn) {
+
+ if (!initialize()) {
+ mErrorList.add("(LOCAL) Cannot bind to ApgService");
+ mResult.putInt(ret.ERROR.name(), error.CANNOT_BIND_TO_APG.ordinal());
+ return false;
+ }
+
+ if (function == null || function.length() == 0) {
+ mErrorList.add("(LOCAL) Function to call missing");
+ mResult.putInt(ret.ERROR.name(), error.CALL_MISSING.ordinal());
+ return false;
+ }
+
+ try {
+ Boolean success = (Boolean) IApgService.class.getMethod(function, Bundle.class, Bundle.class).invoke(mApgService, pArgs, pReturn);
+ mErrorList.addAll(pReturn.getStringArrayList(ret.ERRORS.name()));
+ mWarningList.addAll(pReturn.getStringArrayList(ret.WARNINGS.name()));
+ return success;
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Remote call not known (" + function + "): " + e.getMessage(), e);
+ mErrorList.add("(LOCAL) Remote call not known (" + function + "): " + e.getMessage());
+ mResult.putInt(ret.ERROR.name(), error.CALL_NOT_KNOWN.ordinal());
+ return false;
+ } catch (InvocationTargetException e) {
+ Throwable orig = e.getTargetException();
+ Log.w(TAG, "Exception of type '" + orig.getClass() + "' on AIDL call '" + function + "': " + orig.getMessage(), orig);
+ mErrorList.add("(LOCAL) Exception of type '" + orig.getClass() + "' on AIDL call '" + function + "': " + orig.getMessage());
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Generic error (" + e.getClass() + "): " + e.getMessage(), e);
+ mErrorList.add("(LOCAL) Generic error (" + e.getClass() + "): " + e.getMessage());
+ mResult.putInt(ret.ERROR.name(), error.GENERIC.ordinal());
+ return false;
+ }
+
+ }
+
+ /**
+ * Set a string argument for APG
+ *
+ * <p>
+ * This defines a string argument for APG's AIDL-interface.
+ * </p>
+ *
+ * <p>
+ * To know what key-value-pairs are possible (or required), take a look into
+ * the IApgService.aidl
+ * </p>
+ *
+ * <p>
+ * Note that parameters are not reseted after a call, so you have to
+ * reset ({@link #clearArgs()}) them manually if you want to.
+ * </p>
+ *
+ *
+ * @param key
+ * the key
+ * @param val
+ * the value
+ *
+ * @see #clearArgs()
+ */
+ public void setArg(String key, String val) {
+ mArgs.putString(key, val);
+ }
+
+ /**
+ * Set a string-array argument for APG
+ *
+ * <p>
+ * If the AIDL-parameter is an {@literal ArrayList<String>}, you have to use
+ * this function.
+ * </p>
+ *
+ * <code>
+ * <pre>
+ * setArg("a key", new String[]{ "entry 1", "entry 2" });
+ * </pre>
+ * </code>
+ *
+ * @param key
+ * the key
+ * @param vals
+ * the value
+ *
+ * @see #setArg(String, String)
+ */
+ public void setArg(String key, String vals[]) {
+ ArrayList<String> list = new ArrayList<String>();
+ for (String val : vals) {
+ list.add(val);
+ }
+ mArgs.putStringArrayList(key, list);
+ }
+
+ /**
+ * Set up a boolean argument for APG
+ *
+ * @param key
+ * the key
+ * @param vals
+ * the value
+ *
+ * @see #setArg(String, String)
+ */
+ public void setArg(String key, boolean val) {
+ mArgs.putBoolean(key, val);
+ }
+
+ /**
+ * Set up a int argument for APG
+ *
+ * @param key
+ * the key
+ * @param vals
+ * the value
+ *
+ * @see #setArg(String, String)
+ */
+ public void setArg(String key, int val) {
+ mArgs.putInt(key, val);
+ }
+
+ /**
+ * Set up a int-array argument for APG
+ * <p>
+ * If the AIDL-parameter is an {@literal ArrayList<Integer>}, you have to
+ * use this function.
+ * </p>
+ *
+ * @param key
+ * the key
+ * @param vals
+ * the value
+ *
+ * @see #setArg(String, String)
+ */
+ public void setArg(String key, int vals[]) {
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ for (int val : vals) {
+ list.add(val);
+ }
+ mArgs.putIntegerArrayList(key, list);
+ }
+
+ /**
+ * Set up binary data to en/decrypt
+ *
+ * @param is
+ * InputStream to get the data from
+ */
+ public void setBlob(InputStream is) {
+ if( LOCAL_LOGD ) Log.d(TAG, "setBlob() called");
+ // 1. get the new contentUri
+ ContentResolver cr = mContext.getContentResolver();
+ Uri contentUri = cr.insert(Uri.parse(BLOB_URI), new ContentValues());
+
+ // 2. insert binary data
+ OutputStream os = null;
+ try {
+ os = cr.openOutputStream(contentUri, "w");
+ } catch( Exception e ) {
+ Log.e(TAG, "... exception on setBlob", e);
+ }
+
+ byte[] buffer = new byte[8];
+ int len = 0;
+ try {
+ while( (len = is.read(buffer)) != -1) {
+ os.write(buffer, 0, len);
+ }
+ if(LOCAL_LOGD) Log.d(TAG, "... write finished, now closing");
+ os.close();
+ } catch (Exception e) {
+ Log.e(TAG, "... error on writing buffer", e);
+ }
+
+ mArgs.putString("BLOB", contentUri.toString() );
+ }
+
+ /**
+ * Clears all arguments
+ *
+ * <p>
+ * Anything the has been set up with the various
+ * {@link #setArg(String, String)} functions is cleared.
+ * </p>
+ *
+ * <p>
+ * Note that any warning, error, callback, result, etc. is NOT cleared with
+ * this.
+ * </p>
+ *
+ * @see #reset()
+ */
+ public void clearArgs() {
+ mArgs.clear();
+ }
+
+ /**
+ * Return the object associated with the key
+ *
+ * @param key
+ * the object's key you want to return
+ * @return an object at position key, or null if not set
+ */
+ public Object getArg(String key) {
+ return mArgs.get(key);
+ }
+
+ /**
+ * Iterates through the errors
+ *
+ * <p>
+ * With this method you can iterate through all errors. The errors are only
+ * returned once and deleted immediately afterwards, so you can only return
+ * each error once.
+ * </p>
+ *
+ * @return a human readable description of a error that happened, or null if
+ * no more errors
+ *
+ * @see #hasNextError()
+ * @see #clearErrors()
+ */
+ public String getNextError() {
+ if (mErrorList.size() != 0)
+ return mErrorList.remove(0);
+ else
+ return null;
+ }
+
+ /**
+ * Check if there are any new errors
+ *
+ * @return true, if there are unreturned errors, false otherwise
+ *
+ * @see #getNextError()
+ */
+ public boolean hasNextError() {
+ return mErrorList.size() != 0;
+ }
+
+ /**
+ * Get the numeric representation of the last error
+ *
+ * <p>
+ * Values <100 mean the error happened locally, values >=100 mean the error
+ * happened at the remote side (APG). See the IApgService.aidl (or get the
+ * human readable description with {@link #getNextError()}) for what
+ * errors >=100 mean.
+ * </p>
+ *
+ * @return the id of the error that happened
+ */
+ public int getError() {
+ if (mResult.containsKey(ret.ERROR.name()))
+ return mResult.getInt(ret.ERROR.name());
+ else
+ return -1;
+ }
+
+ /**
+ * Iterates through the warnings
+ *
+ * <p>
+ * With this method you can iterate through all warnings. Warnings are
+ * only returned once and deleted immediately afterwards, so you can only
+ * return each warning once.
+ * </p>
+ *
+ * @return a human readable description of a warning that happened, or null
+ * if no more warnings
+ *
+ * @see #hasNextWarning()
+ * @see #clearWarnings()
+ */
+ public String getNextWarning() {
+ if (mWarningList.size() != 0)
+ return mWarningList.remove(0);
+ else
+ return null;
+ }
+
+ /**
+ * Check if there are any new warnings
+ *
+ * @return true, if there are unreturned warnings, false otherwise
+ *
+ * @see #getNextWarning()
+ */
+ public boolean hasNextWarning() {
+ return mWarningList.size() != 0;
+ }
+
+ /**
+ * Get the result
+ *
+ * <p>
+ * This gets your result. After doing an encryption or decryption with APG,
+ * you get the output with this function.
+ * </p>
+ *
+ * <p>
+ * Note when your last remote call is unsuccessful, the result will
+ * still have the same value like the last successful call (or null, if no
+ * call was successful). To ensure you do not work with old call's results,
+ * either be sure to {@link #reset()} (or at least {@link #clearResult()})
+ * your instance before each new call or always check that
+ * {@link #hasNextError()} is false.
+ * </p>
+ *
+ * <p>
+ * Note: When handling binary data with {@link #setBlob(InputStream)}, you
+ * get your result with {@link #getBlobResult()}.
+ * </p>
+ *
+ * @return the mResult of the last {@link #call(String)} or
+ * {@link #callAsync(String)}.
+ *
+ * @see #reset()
+ * @see #clearResult()
+ * @see #getResultBundle()
+ * @see #getBlobResult()
+ */
+ public String getResult() {
+ return mResult.getString(ret.RESULT.name());
+ }
+
+ /**
+ * Get the binary result
+ *
+ * <p>
+ * This gets your binary result. It only works if you called {@link #setBlob(InputStream)} before.
+ *
+ * If you did not call encrypt nor decrypt, this will be the same data as you inputed.
+ * </p>
+ *
+ * @return InputStream of the binary data which was en/decrypted
+ *
+ * @see #setBlob(InputStream)
+ * @see #getResult()
+ */
+ public InputStream getBlobResult() {
+ if(mArgs.containsKey("BLOB")) {
+ ContentResolver cr = mContext.getContentResolver();
+ InputStream in = null;
+ try {
+ in = cr.openInputStream(Uri.parse(mArgs.getString("BLOB")));
+ } catch( Exception e ) {
+ Log.e(TAG, "Could not return blob in result", e);
+ }
+ return in;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the result bundle
+ *
+ * <p>
+ * Unlike {@link #getResult()}, which only returns any en-/decrypted
+ * message, this function returns the complete information that was returned
+ * by Apg. This also includes the "RESULT", but additionally the warnings,
+ * errors and any other information.
+ * </p>
+ * <p>
+ * For warnings and errors it is suggested to use the functions that are
+ * provided here, namely {@link #getError()}, {@link #getNextError()},
+ * {@link #get_next_Warning()} etc.), but if any call returns something non
+ * standard, you have access to the complete result bundle to extract the
+ * information.
+ * </p>
+ *
+ * @return the complete result bundle of the last call to apg
+ */
+ public Bundle getResultBundle() {
+ return mResult;
+ }
+
+ public error getConnectionStatus() {
+ return mConnectionStatus;
+ }
+
+ /**
+ * Clears all unfetched errors
+ *
+ * @see #getNextError()
+ * @see #hasNextError()
+ */
+ public void clearErrors() {
+ mErrorList.clear();
+ mResult.remove(ret.ERROR.name());
+ }
+
+ /**
+ * Clears all unfetched warnings
+ *
+ * @see #getNextWarning()
+ * @see #hasNextWarning()
+ */
+ public void clearWarnings() {
+ mWarningList.clear();
+ }
+
+ /**
+ * Clears the last mResult
+ *
+ * @see #getResult()
+ */
+ public void clearResult() {
+ mResult.remove(ret.RESULT.name());
+ }
+
+ /**
+ * Set a callback listener when call to AIDL finishes
+ *
+ * @param obj
+ * a object to call back after async execution
+ * @see ApgConInterface
+ */
+ public void setOnCallFinishListener(OnCallFinishListener lis) {
+ mOnCallFinishListener = lis;
+ }
+
+ /**
+ * Clears any callback object
+ *
+ * @see #setOnCallFinishListener(OnCallFinishListener)
+ */
+ public void clearOnCallFinishListener() {
+ mOnCallFinishListener = null;
+ }
+
+ /**
+ * Checks if an async execution is running
+ *
+ * <p>
+ * If you started something with {@link #callAsync(String)}, this will
+ * return true if the task is still running
+ * </p>
+ *
+ * @return true, if an async task is still running, false otherwise
+ *
+ * @see #callAsync(String)
+ *
+ */
+ public boolean isRunning() {
+ return mAsyncRunning;
+ }
+
+ /**
+ * Completely resets your instance
+ *
+ * <p>
+ * This currently resets everything in this instance. Errors, warnings,
+ * results, callbacks, ... are removed. Any connection to the remote
+ * interface is upheld, though.
+ * </p>
+ *
+ * <p>
+ * Note when an async execution ({@link #callAsync(String)}) is
+ * running, it's result, warnings etc. will still be evaluated (which might
+ * be not what you want). Also mind that any callback you set is also
+ * reseted, so when finishing the execution any before defined callback will
+ * NOT BE TRIGGERED.
+ * </p>
+ */
+ public void reset() {
+ clearErrors();
+ clearWarnings();
+ clearArgs();
+ clearOnCallFinishListener();
+ mResult.clear();
+ }
+
+}
diff --git a/org_apg/src/org/apg/util/ApgConInterface.java b/org_apg/src/org/apg/util/ApgConInterface.java
new file mode 100644
index 000000000..2b66164f1
--- /dev/null
+++ b/org_apg/src/org/apg/util/ApgConInterface.java
@@ -0,0 +1,7 @@
+package org.apg.util;
+
+public interface ApgConInterface {
+ public static interface OnCallFinishListener {
+ public abstract void onCallFinish(android.os.Bundle result);
+ }
+}
diff --git a/org_apg/src/org/apg/util/Choice.java b/org_apg/src/org/apg/util/Choice.java
new file mode 100644
index 000000000..1dbd9b215
--- /dev/null
+++ b/org_apg/src/org/apg/util/Choice.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.util;
+
+public class Choice {
+ private String mName;
+ private int mId;
+
+ public Choice() {
+ mId = -1;
+ mName = "";
+ }
+
+ public Choice(int id, String name) {
+ mId = id;
+ mName = name;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+}
diff --git a/org_apg/src/org/apg/util/Compatibility.java b/org_apg/src/org/apg/util/Compatibility.java
new file mode 100644
index 000000000..10b3c7f6b
--- /dev/null
+++ b/org_apg/src/org/apg/util/Compatibility.java
@@ -0,0 +1,79 @@
+package org.apg.util;
+
+import java.lang.reflect.Method;
+
+import android.content.Context;
+import android.util.Log;
+
+public class Compatibility {
+
+ private static final String clipboardLabel = "APG";
+
+ /**
+ * Wrapper around ClipboardManager based on Android version using Reflection API, from
+ * http://www.projectsexception.com/blog/?p=87
+ *
+ * @param context
+ * @param text
+ */
+ public static void copyToClipboard(Context context, String text) {
+ Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
+ Method method = clipboard.getClass().getMethod("setText", CharSequence.class);
+ method.invoke(clipboard, text);
+ } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
+ Class<?> clazz = Class.forName("android.content.ClipData");
+ Method method = clazz.getMethod("newPlainText", CharSequence.class,
+ CharSequence.class);
+ Object clip = method.invoke(null, clipboardLabel, text);
+ method = clipboard.getClass().getMethod("setPrimaryClip", clazz);
+ method.invoke(clipboard, clip);
+ }
+ } catch (Exception e) {
+ Log.e("ProjectsException", "There was and error copying the text to the clipboard: "
+ + e.getMessage());
+ }
+ }
+
+ /**
+ * Wrapper around ClipboardManager based on Android version using Reflection API
+ *
+ * @param context
+ * @param text
+ */
+ public static CharSequence getClipboardText(Context context) {
+ Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
+ // CharSequence text = clipboard.getText();
+ Method method = clipboard.getClass().getMethod("getText");
+ Object text = method.invoke(clipboard);
+
+ return (CharSequence) text;
+ } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
+ // ClipData clipData = clipboard.getPrimaryClip();
+ Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip");
+ Object clipData = methodGetPrimaryClip.invoke(clipboard);
+
+ // ClipData.Item clipDataItem = clipData.getItemAt(0);
+ Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", Integer.TYPE);
+ Object clipDataItem = methodGetItemAt.invoke(clipData, 0);
+
+ // CharSequence text = clipDataItem.coerceToText(context);
+ Method methodGetString = clipDataItem.getClass().getMethod("coerceToText",
+ Context.class);
+ Object text = methodGetString.invoke(clipDataItem, context);
+
+ return (CharSequence) text;
+ } else {
+ return null;
+ }
+ } catch (Exception e) {
+ Log.e("ProjectsException", "There was and error getting the text from the clipboard: "
+ + e.getMessage());
+
+ return null;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/util/IterableIterator.java b/org_apg/src/org/apg/util/IterableIterator.java
new file mode 100644
index 000000000..be6632ac0
--- /dev/null
+++ b/org_apg/src/org/apg/util/IterableIterator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.util;
+
+import java.util.Iterator;
+
+public class IterableIterator<T> implements Iterable<T> {
+ private Iterator<T> mIter;
+
+ public IterableIterator(Iterator<T> iter) {
+ mIter = iter;
+ }
+
+ public Iterator<T> iterator() {
+ return mIter;
+ }
+}