aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src')
-rw-r--r--OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java1
-rw-r--r--OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java76
-rw-r--r--OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java278
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java23
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java69
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java30
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java7
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java22
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java225
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesInputFragment.java144
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesListFragment.java623
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java11
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java163
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml149
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml83
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml50
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_list_entry.xml248
-rw-r--r--OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml22
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml2
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java12
22 files changed, 1546 insertions, 710 deletions
diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java
index f85523eb7..e81b312ac 100644
--- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java
+++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java
@@ -65,7 +65,6 @@ public class CreateKeyActivityTest extends ActivityInstrumentationTestCase2<Crea
getActivity();
}
- @Test
public void testCreateMyKey() {
// Clicks create my key
onView(withId(R.id.create_key_create_key_button))
diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java
new file mode 100644
index 000000000..d35f1d751
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann
+ *
+ * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details.
+ */
+
+package org.spongycastle.openpgp.operator.jcajce;
+
+import org.spongycastle.jcajce.util.NamedJcaJceHelper;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.spongycastle.openpgp.operator.PGPDataDecryptor;
+import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory
+{
+ private final PublicKeyDataDecryptorFactory mWrappedDecryptor;
+ private final Map<ByteBuffer, byte[]> mSessionKeyCache;
+
+ private OperatorHelper mOperatorHelper;
+
+ public CachingDataDecryptorFactory(String providerName,
+ final Map<ByteBuffer,byte[]> sessionKeyCache)
+ {
+ mWrappedDecryptor = null;
+ mSessionKeyCache = sessionKeyCache;
+
+ mOperatorHelper = new OperatorHelper(new NamedJcaJceHelper(providerName));
+ }
+
+ public CachingDataDecryptorFactory(PublicKeyDataDecryptorFactory wrapped,
+ final Map<ByteBuffer,byte[]> sessionKeyCache)
+ {
+ mWrappedDecryptor = wrapped;
+ mSessionKeyCache = sessionKeyCache;
+
+ }
+
+ public boolean hasCachedSessionData(PGPPublicKeyEncryptedData encData) throws PGPException {
+ ByteBuffer bi = ByteBuffer.wrap(encData.getSessionKey()[0]);
+ return mSessionKeyCache.containsKey(bi);
+ }
+
+ public Map<ByteBuffer, byte[]> getCachedSessionKeys() {
+ return mSessionKeyCache;
+ }
+
+ public boolean canDecrypt() {
+ return mWrappedDecryptor != null;
+ }
+
+ @Override
+ public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException {
+ ByteBuffer bi = ByteBuffer.wrap(secKeyData[0]); // encoded MPI
+ if (mSessionKeyCache.containsKey(bi)) {
+ return mSessionKeyCache.get(bi);
+ }
+
+ byte[] sessionData = mWrappedDecryptor.recoverSessionData(keyAlgorithm, secKeyData);
+ mSessionKeyCache.put(bi, sessionData);
+ return sessionData;
+ }
+
+ @Override
+ public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
+ throws PGPException {
+ if (mWrappedDecryptor != null) {
+ return mWrappedDecryptor.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
+ }
+ return mOperatorHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java
deleted file mode 100644
index 067bb3e19..000000000
--- a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann
- *
- * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details.
- */
-
-package org.spongycastle.openpgp.operator.jcajce;
-
-import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
-import org.spongycastle.jcajce.util.DefaultJcaJceHelper;
-import org.spongycastle.jcajce.util.NamedJcaJceHelper;
-import org.spongycastle.jcajce.util.ProviderJcaJceHelper;
-import org.spongycastle.openpgp.PGPException;
-import org.spongycastle.openpgp.PGPPublicKey;
-import org.spongycastle.openpgp.operator.PGPDataDecryptor;
-import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
-
-import java.nio.ByteBuffer;
-import java.security.Provider;
-import java.util.Map;
-
-
-/**
- * This class is based on JcePublicKeyDataDecryptorFactoryBuilder
- *
- */
-public class NfcSyncPublicKeyDataDecryptorFactoryBuilder
-{
- private OperatorHelper helper = new OperatorHelper(new DefaultJcaJceHelper());
- private OperatorHelper contentHelper = new OperatorHelper(new DefaultJcaJceHelper());
- private JcaPGPKeyConverter keyConverter = new JcaPGPKeyConverter();
-// private JcaPGPDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaPGPDigestCalculatorProviderBuilder();
-// private JcaKeyFingerprintCalculator fingerprintCalculator = new JcaKeyFingerprintCalculator();
-
- public static class NfcInteractionNeeded extends RuntimeException
- {
- public byte[] encryptedSessionKey;
-
- public NfcInteractionNeeded(byte[] encryptedSessionKey)
- {
- super("NFC interaction required!");
- this.encryptedSessionKey = encryptedSessionKey;
- }
- }
-
- public NfcSyncPublicKeyDataDecryptorFactoryBuilder()
- {
- }
-
- /**
- * Set the provider object to use for creating cryptographic primitives in the resulting factory the builder produces.
- *
- * @param provider provider object for cryptographic primitives.
- * @return the current builder.
- */
- public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(Provider provider)
- {
- this.helper = new OperatorHelper(new ProviderJcaJceHelper(provider));
- keyConverter.setProvider(provider);
- this.contentHelper = helper;
-
- return this;
- }
-
- /**
- * Set the provider name to use for creating cryptographic primitives in the resulting factory the builder produces.
- *
- * @param providerName the name of the provider to reference for cryptographic primitives.
- * @return the current builder.
- */
- public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(String providerName)
- {
- this.helper = new OperatorHelper(new NamedJcaJceHelper(providerName));
- keyConverter.setProvider(providerName);
- this.contentHelper = helper;
-
- return this;
- }
-
- public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(Provider provider)
- {
- this.contentHelper = new OperatorHelper(new ProviderJcaJceHelper(provider));
-
- return this;
- }
-
- public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(String providerName)
- {
- this.contentHelper = new OperatorHelper(new NamedJcaJceHelper(providerName));
-
- return this;
- }
-
- public PublicKeyDataDecryptorFactory build(final Map<ByteBuffer,byte[]> nfcDecryptedMap) {
- return new PublicKeyDataDecryptorFactory()
- {
- public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData)
- throws PGPException
- {
- if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH)
- {
- throw new PGPException("ECDH not supported!");
- }
-
- return decryptSessionData(keyAlgorithm, secKeyData, nfcDecryptedMap);
- }
-
- public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
- throws PGPException
- {
- return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
- }
- };
- }
-
-// public PublicKeyDataDecryptorFactory build(final PrivateKey privKey)
-// {
-// return new PublicKeyDataDecryptorFactory()
-// {
-// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData)
-// throws PGPException
-// {
-// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH)
-// {
-// throw new PGPException("ECDH requires use of PGPPrivateKey for decryption");
-// }
-// return decryptSessionData(keyAlgorithm, privKey, secKeyData);
-// }
-//
-// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
-// throws PGPException
-// {
-// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
-// }
-// };
-// }
-
-// public PublicKeyDataDecryptorFactory build(final PGPPrivateKey privKey, final byte[] nfcDecrypted)
-// {
-// return new PublicKeyDataDecryptorFactory()
-// {
-// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData)
-// throws PGPException
-// {
-// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH)
-// {
-// return decryptSessionData(privKey.getPrivateKeyDataPacket(), privKey.getPublicKeyPacket(), secKeyData);
-// }
-//
-// return decryptSessionData(keyAlgorithm, keyConverter.getPrivateKey(privKey), secKeyData, nfcDecrypted);
-// }
-//
-// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
-// throws PGPException
-// {
-// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
-// }
-// };
-// }
-
-// private byte[] decryptSessionData(BCPGKey privateKeyPacket, PublicKeyPacket pubKeyData, byte[][] secKeyData)
-// throws PGPException
-// {
-// ECDHPublicBCPGKey ecKey = (ECDHPublicBCPGKey)pubKeyData.getKey();
-// X9ECParameters x9Params = NISTNamedCurves.getByOID(ecKey.getCurveOID());
-//
-// byte[] enc = secKeyData[0];
-//
-// int pLen = ((((enc[0] & 0xff) << 8) + (enc[1] & 0xff)) + 7) / 8;
-// byte[] pEnc = new byte[pLen];
-//
-// System.arraycopy(enc, 2, pEnc, 0, pLen);
-//
-// byte[] keyEnc = new byte[enc[pLen + 2]];
-//
-// System.arraycopy(enc, 2 + pLen + 1, keyEnc, 0, keyEnc.length);
-//
-// Cipher c = helper.createKeyWrapper(ecKey.getSymmetricKeyAlgorithm());
-//
-// ECPoint S = x9Params.getCurve().decodePoint(pEnc).multiply(((ECSecretBCPGKey)privateKeyPacket).getX()).normalize();
-//
-// RFC6637KDFCalculator rfc6637KDFCalculator = new RFC6637KDFCalculator(digestCalculatorProviderBuilder.build().get(ecKey.getHashAlgorithm()), ecKey.getSymmetricKeyAlgorithm());
-// Key key = new SecretKeySpec(rfc6637KDFCalculator.createKey(ecKey.getCurveOID(), S, fingerprintCalculator.calculateFingerprint(pubKeyData)), "AESWrap");
-//
-// try
-// {
-// c.init(Cipher.UNWRAP_MODE, key);
-//
-// Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY);
-//
-// return PGPPad.unpadSessionData(paddedSessionKey.getEncoded());
-// }
-// catch (InvalidKeyException e)
-// {
-// throw new PGPException("error setting asymmetric cipher", e);
-// }
-// catch (NoSuchAlgorithmException e)
-// {
-// throw new PGPException("error setting asymmetric cipher", e);
-// }
-// }
-
- private byte[] decryptSessionData(int keyAlgorithm, byte[][] secKeyData,
- Map<ByteBuffer,byte[]> nfcDecryptedMap)
- throws PGPException
- {
-// Cipher c1 = helper.createPublicKeyCipher(keyAlgorithm);
-//
-// try
-// {
-// c1.init(Cipher.DECRYPT_MODE, privKey);
-// }
-// catch (InvalidKeyException e)
-// {
-// throw new PGPException("error setting asymmetric cipher", e);
-// }
-
- if (keyAlgorithm == PGPPublicKey.RSA_ENCRYPT
- || keyAlgorithm == PGPPublicKey.RSA_GENERAL)
- {
- ByteBuffer bi = ByteBuffer.wrap(secKeyData[0]); // encoded MPI
-
- if (nfcDecryptedMap.containsKey(bi)) {
- return nfcDecryptedMap.get(bi);
- } else {
- // catch this when decryptSessionData() is executed and divert digest to card,
- // when doing the operation again reuse nfcDecrypted
- throw new NfcInteractionNeeded(bi.array());
- }
-
-// c1.update(bi, 2, bi.length - 2);
- }
- else
- {
- throw new PGPException("ElGamal not supported!");
-
-// ElGamalKey k = (ElGamalKey)privKey;
-// int size = (k.getParameters().getP().bitLength() + 7) / 8;
-// byte[] tmp = new byte[size];
-//
-// byte[] bi = secKeyData[0]; // encoded MPI
-// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but...
-// {
-// c1.update(bi, 3, bi.length - 3);
-// }
-// else
-// {
-// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2);
-// c1.update(tmp);
-// }
-//
-// bi = secKeyData[1]; // encoded MPI
-// for (int i = 0; i != tmp.length; i++)
-// {
-// tmp[i] = 0;
-// }
-//
-// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but...
-// {
-// c1.update(bi, 3, bi.length - 3);
-// }
-// else
-// {
-// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2);
-// c1.update(tmp);
-// }
- }
-
-// try
-// {
-// return c1.doFinal();
-// }
-// catch (Exception e)
-// {
-// throw new PGPException("exception decrypting session data", e);
-// }
- }
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java
index ac571390a..25a86f137 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java
@@ -22,6 +22,7 @@ import android.os.Parcel;
import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.util.Passphrase;
@@ -36,6 +37,8 @@ public class DecryptVerifyResult extends InputPendingResult {
// https://tools.ietf.org/html/rfc4880#page56
String mCharset;
+ CryptoInputParcel mCachedCryptoInputParcel;
+
byte[] mOutputBytes;
public DecryptVerifyResult(int result, OperationLog log) {
@@ -50,6 +53,7 @@ public class DecryptVerifyResult extends InputPendingResult {
super(source);
mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader());
mDecryptMetadata = source.readParcelable(OpenPgpMetadata.class.getClassLoader());
+ mCachedCryptoInputParcel = source.readParcelable(CryptoInputParcel.class.getClassLoader());
}
@@ -65,6 +69,14 @@ public class DecryptVerifyResult extends InputPendingResult {
mSignatureResult = signatureResult;
}
+ public CryptoInputParcel getCachedCryptoInputParcel() {
+ return mCachedCryptoInputParcel;
+ }
+
+ public void setCachedCryptoInputParcel(CryptoInputParcel cachedCryptoInputParcel) {
+ mCachedCryptoInputParcel = cachedCryptoInputParcel;
+ }
+
public OpenPgpMetadata getDecryptMetadata() {
return mDecryptMetadata;
}
@@ -97,6 +109,7 @@ public class DecryptVerifyResult extends InputPendingResult {
super.writeToParcel(dest, flags);
dest.writeParcelable(mSignatureResult, 0);
dest.writeParcelable(mDecryptMetadata, 0);
+ dest.writeParcelable(mCachedCryptoInputParcel, 0);
}
public static final Creator<DecryptVerifyResult> CREATOR = new Creator<DecryptVerifyResult>() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java
index 17d342341..31a3925da 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java
@@ -21,23 +21,18 @@ package org.sufficientlysecure.keychain.pgp;
import org.spongycastle.bcpg.S2K;
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.PGPUserAttributeSubpacketVector;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
-import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyConverter;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
-import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFactoryBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
@@ -51,7 +46,6 @@ import java.security.interfaces.RSAPrivateCrtKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
@@ -270,19 +264,20 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey {
}
}
- public PublicKeyDataDecryptorFactory getDecryptorFactory(CryptoInputParcel cryptoInput) {
+ public CachingDataDecryptorFactory getCachingDecryptorFactory(CryptoInputParcel cryptoInput) {
if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) {
throw new PrivateKeyNotUnlockedException();
}
if (mPrivateKeyState == PRIVATE_KEY_STATE_DIVERT_TO_CARD) {
- return new NfcSyncPublicKeyDataDecryptorFactoryBuilder()
- .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
- cryptoInput.getCryptoData()
- );
+ return new CachingDataDecryptorFactory(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME,
+ cryptoInput.getCryptoData());
} else {
- return new JcePublicKeyDataDecryptorFactoryBuilder()
- .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey);
+ return new CachingDataDecryptorFactory(
+ new JcePublicKeyDataDecryptorFactoryBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey),
+ cryptoInput.getCryptoData());
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
index e3059defb..6a85ce251 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
@@ -40,11 +40,10 @@ import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
-import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
-import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFactoryBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.BaseOperation;
@@ -538,24 +537,33 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
- try {
- PublicKeyDataDecryptorFactory decryptorFactory
- = secretEncryptionKey.getDecryptorFactory(cryptoInput);
- try {
- clear = encryptedDataAsymmetric.getDataStream(decryptorFactory);
- } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) {
- log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
- }
+ CachingDataDecryptorFactory decryptorFactory
+ = secretEncryptionKey.getCachingDecryptorFactory(cryptoInput);
+
+ // special case: if the decryptor does not have a session key cached for this encrypted
+ // data, and can't actually decrypt on its own, return a pending intent
+ if (!decryptorFactory.canDecrypt()
+ && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
- symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
- } catch (NfcSyncPublicKeyDataDecryptorFactoryBuilder.NfcInteractionNeeded e) {
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
secretEncryptionKey.getRing().getMasterKeyId(),
- secretEncryptionKey.getKeyId(), e.encryptedSessionKey
+ secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
));
+
+ }
+
+ try {
+ clear = encryptedDataAsymmetric.getDataStream(decryptorFactory);
+ } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) {
+ log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1);
+ return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
+
+ symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
+
+ cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
+
encryptedData = encryptedDataAsymmetric;
} else {
// there wasn't even any useful data
@@ -662,9 +670,6 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
PGPLiteralData literalData = (PGPLiteralData) dataChunk;
- // reported size may be null if partial packets are involved (highly unlikely though)
- Long originalSize = literalData.getDataLengthIfAvailable();
-
String originalFilename = literalData.getFileName();
String mimeType = null;
if (literalData.getFormat() == PGPLiteralData.TEXT
@@ -687,12 +692,6 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
}
}
- metadata = new OpenPgpMetadata(
- originalFilename,
- mimeType,
- literalData.getModificationTime().getTime(),
- originalSize == null ? 0 : originalSize);
-
if (!"".equals(originalFilename)) {
log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
}
@@ -700,15 +699,26 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
mimeType);
log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
new Date(literalData.getModificationTime().getTime()).toString());
- if (originalSize != null) {
- log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
- Long.toString(originalSize));
- } else {
- log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
- }
// return here if we want to decrypt the metadata only
if (input.isDecryptMetadataOnly()) {
+
+ // this operation skips the entire stream to find the data length!
+ Long originalSize = literalData.findDataLength();
+
+ if (originalSize != null) {
+ log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
+ Long.toString(originalSize));
+ } else {
+ log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
+ }
+
+ metadata = new OpenPgpMetadata(
+ originalFilename,
+ mimeType,
+ literalData.getModificationTime().getTime(),
+ originalSize == null ? 0 : originalSize);
+
log.add(LogType.MSG_DC_OK_META_ONLY, indent);
DecryptVerifyResult result =
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
@@ -824,6 +834,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
// Return a positive result, with metadata and verification info
DecryptVerifyResult result =
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
+ result.setCachedCryptoInputParcel(cryptoInput);
result.setDecryptMetadata(metadata);
result.setSignatureResult(signatureResultBuilder.build());
result.setCharset(charset);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java
index 2000a6525..74f7c6513 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java
@@ -57,6 +57,11 @@ public class TemporaryStorageProvider extends ContentProvider {
return context.getContentResolver().insert(BASE_URI, contentValues);
}
+ public static Uri createFile(Context context) {
+ ContentValues contentValues = new ContentValues();
+ return context.getContentResolver().insert(BASE_URI, contentValues);
+ }
+
public static int cleanUp(Context context) {
return context.getContentResolver().delete(BASE_URI, COLUMN_TIME + "< ?",
new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)});
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java
index 76aa1a618..8b90e41bf 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java
@@ -128,17 +128,18 @@ public class ServiceProgressHandler extends Handler {
case UPDATE_PROGRESS:
if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) {
+ String msg = null;
+ int progress = data.getInt(DATA_PROGRESS), max =data.getInt(DATA_PROGRESS_MAX);
+
// update progress from service
if (data.containsKey(DATA_MESSAGE)) {
- progressDialogFragment.setProgress(data.getString(DATA_MESSAGE),
- data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
+ msg = data.getString(DATA_MESSAGE);
} else if (data.containsKey(DATA_MESSAGE_ID)) {
- progressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID),
- data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
- } else {
- progressDialogFragment.setProgress(data.getInt(DATA_PROGRESS),
- data.getInt(DATA_PROGRESS_MAX));
+ msg = mActivity.getString(data.getInt(DATA_MESSAGE_ID));
}
+
+ onSetProgress(msg, progress, max);
+
}
break;
@@ -152,4 +153,19 @@ public class ServiceProgressHandler extends Handler {
break;
}
}
+
+ protected void onSetProgress(String msg, int progress, int max) {
+
+ ProgressDialogFragment progressDialogFragment =
+ (ProgressDialogFragment) mActivity.getSupportFragmentManager()
+ .findFragmentByTag("progressDialog");
+
+ if (msg != null) {
+ progressDialogFragment.setProgress(msg, progress, max);
+ } else {
+ progressDialogFragment.setProgress(progress, max);
+ }
+
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
index 3d1ccaca1..ee7caf2d8 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
@@ -97,8 +97,12 @@ public class CryptoInputParcel implements Parcelable {
mCryptoData.put(ByteBuffer.wrap(hash), signedHash);
}
+ public void addCryptoData(Map<ByteBuffer, byte[]> cachedSessionKeys) {
+ mCryptoData.putAll(cachedSessionKeys);
+ }
+
public Map<ByteBuffer, byte[]> getCryptoData() {
- return Collections.unmodifiableMap(mCryptoData);
+ return mCryptoData;
}
public Date getSignatureTime() {
@@ -138,4 +142,5 @@ public class CryptoInputParcel implements Parcelable {
b.append("}");
return b.toString();
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java
index 81fb6a392..b56c38d19 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java
@@ -17,11 +17,12 @@
package org.sufficientlysecure.keychain.ui;
+import java.util.ArrayList;
+
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.Toast;
@@ -29,7 +30,7 @@ import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.intents.OpenKeychainIntents;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
-import org.sufficientlysecure.keychain.util.Log;
+
public class DecryptFilesActivity extends BaseActivity {
@@ -94,13 +95,26 @@ public class DecryptFilesActivity extends BaseActivity {
}
boolean showOpenDialog = ACTION_DECRYPT_DATA_OPEN.equals(action);
- DecryptFilesFragment frag = DecryptFilesFragment.newInstance(uri, showOpenDialog);
+ DecryptFilesInputFragment frag = DecryptFilesInputFragment.newInstance(uri, showOpenDialog);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
getSupportFragmentManager().beginTransaction()
.replace(R.id.decrypt_files_fragment_container, frag)
- .commitAllowingStateLoss();
+ .commit();
+
+ }
+
+ public void displayListFragment(Uri inputUri) {
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(inputUri);
+ DecryptFilesListFragment frag = DecryptFilesListFragment.newInstance(uris);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.decrypt_files_fragment_container, frag)
+ .addToBackStack("list")
+ .commit();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java
deleted file mode 100644
index bc7705233..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sufficientlysecure.keychain.ui;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Message;
-import android.os.Messenger;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.TextView;
-
-import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
-import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
-import org.sufficientlysecure.keychain.service.KeychainService;
-import org.sufficientlysecure.keychain.service.ServiceProgressHandler;
-import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
-import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
-import org.sufficientlysecure.keychain.ui.util.Notify;
-import org.sufficientlysecure.keychain.util.FileHelper;
-import org.sufficientlysecure.keychain.util.Log;
-
-import java.io.File;
-
-public class DecryptFilesFragment extends DecryptFragment {
- public static final String ARG_URI = "uri";
- public static final String ARG_OPEN_DIRECTLY = "open_directly";
-
- private static final int REQUEST_CODE_INPUT = 0x00007003;
- private static final int REQUEST_CODE_OUTPUT = 0x00007007;
-
- // view
- private TextView mFilename;
- private CheckBox mDeleteAfter;
- private View mDecryptButton;
-
- // model
- private Uri mInputUri = null;
- private Uri mOutputUri = null;
-
- /**
- * Creates new instance of this fragment
- */
- public static DecryptFilesFragment newInstance(Uri uri, boolean openDirectly) {
- DecryptFilesFragment frag = new DecryptFilesFragment();
-
- Bundle args = new Bundle();
- args.putParcelable(ARG_URI, uri);
- args.putBoolean(ARG_OPEN_DIRECTLY, openDirectly);
-
- frag.setArguments(args);
-
- return frag;
- }
-
- /**
- * Inflate the layout for this fragment
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.decrypt_files_fragment, container, false);
-
- mFilename = (TextView) view.findViewById(R.id.decrypt_files_filename);
- mDeleteAfter = (CheckBox) view.findViewById(R.id.decrypt_files_delete_after_decryption);
- mDecryptButton = view.findViewById(R.id.decrypt_files_action_decrypt);
- view.findViewById(R.id.decrypt_files_browse).setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT);
- } else {
- FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*",
- REQUEST_CODE_INPUT);
- }
- }
- });
- mDecryptButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- decryptAction();
- }
- });
-
- return view;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putParcelable(ARG_URI, mInputUri);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- Bundle state = savedInstanceState != null ? savedInstanceState : getArguments();
- setInputUri(state.<Uri>getParcelable(ARG_URI));
-
- // should only come from args
- if (state.getBoolean(ARG_OPEN_DIRECTLY, false)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT);
- } else {
- FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*", REQUEST_CODE_INPUT);
- }
- }
- }
-
- private void setInputUri(Uri inputUri) {
- if (inputUri == null) {
- mInputUri = null;
- mFilename.setText("");
- return;
- }
-
- mInputUri = inputUri;
- mFilename.setText(FileHelper.getFilename(getActivity(), mInputUri));
- }
-
- private void decryptAction() {
- if (mInputUri == null) {
- Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show();
- return;
- }
-
- cryptoOperation();
- }
-
- private String removeEncryptedAppend(String name) {
- if (name.endsWith(Constants.FILE_EXTENSION_ASC)
- || name.endsWith(Constants.FILE_EXTENSION_PGP_MAIN)
- || name.endsWith(Constants.FILE_EXTENSION_PGP_ALTERNATE)) {
- return name.substring(0, name.length() - 4);
- }
- return name;
- }
-
- private void askForOutputFilename(String originalFilename) {
- if (TextUtils.isEmpty(originalFilename)) {
- originalFilename = removeEncryptedAppend(FileHelper.getFilename(getActivity(), mInputUri));
- }
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- File file = new File(mInputUri.getPath());
- File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
- File targetFile = new File(parentDir, originalFilename);
- FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
- getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
- } else {
- FileHelper.saveDocument(this, "*/*", originalFilename, REQUEST_CODE_OUTPUT);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_CODE_INPUT: {
- if (resultCode == Activity.RESULT_OK && data != null) {
- setInputUri(data.getData());
- }
- return;
- }
-
- case REQUEST_CODE_OUTPUT: {
- // This happens after output file was selected, so start our operation
- if (resultCode == Activity.RESULT_OK && data != null) {
- mOutputUri = data.getData();
- cryptoOperation();
- }
- return;
- }
-
- default: {
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
- }
-
- @Override
- protected void onVerifyLoaded(boolean hideErrorOverlay) {
-
- }
-
- @Override
- protected PgpDecryptVerifyInputParcel createOperationInput() {
- return new PgpDecryptVerifyInputParcel(mInputUri, mOutputUri).setAllowSymmetricDecryption(true);
- }
-
- @Override
- protected void onCryptoOperationSuccess(DecryptVerifyResult result) {
-
- // display signature result in activity
- loadVerifyResult(result);
-
- // TODO delete after decrypt not implemented!
-
- }
-
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesInputFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesInputFragment.java
new file mode 100644
index 000000000..5b0b191e0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesInputFragment.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <look@my.amazin.horse>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+public class DecryptFilesInputFragment extends Fragment {
+ public static final String ARG_URI = "uri";
+ public static final String ARG_OPEN_DIRECTLY = "open_directly";
+
+ private static final int REQUEST_CODE_INPUT = 0x00007003;
+
+ private TextView mFilename;
+ private View mDecryptButton;
+
+ private Uri mInputUri = null;
+
+ public static DecryptFilesInputFragment newInstance(Uri uri, boolean openDirectly) {
+ DecryptFilesInputFragment frag = new DecryptFilesInputFragment();
+
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_URI, uri);
+ args.putBoolean(ARG_OPEN_DIRECTLY, openDirectly);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.decrypt_files_input_fragment, container, false);
+
+ // hide result view for this fragment
+ getActivity().findViewById(R.id.result_main_layout).setVisibility(View.GONE);
+
+ mFilename = (TextView) view.findViewById(R.id.decrypt_files_filename);
+ mDecryptButton = view.findViewById(R.id.decrypt_files_action_decrypt);
+ view.findViewById(R.id.decrypt_files_browse).setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ FileHelper.openDocument(DecryptFilesInputFragment.this, "*/*", REQUEST_CODE_INPUT);
+ } else {
+ FileHelper.openFile(DecryptFilesInputFragment.this, mInputUri, "*/*",
+ REQUEST_CODE_INPUT);
+ }
+ }
+ });
+ mDecryptButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ decryptAction();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putParcelable(ARG_URI, mInputUri);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Bundle state = savedInstanceState != null ? savedInstanceState : getArguments();
+ setInputUri(state.<Uri>getParcelable(ARG_URI));
+
+ // should only come from args
+ if (state.getBoolean(ARG_OPEN_DIRECTLY, false)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ FileHelper.openDocument(DecryptFilesInputFragment.this, "*/*", REQUEST_CODE_INPUT);
+ } else {
+ FileHelper.openFile(DecryptFilesInputFragment.this, mInputUri, "*/*", REQUEST_CODE_INPUT);
+ }
+ }
+ }
+
+ private void setInputUri(Uri inputUri) {
+ if (inputUri == null) {
+ mInputUri = null;
+ mFilename.setText("");
+ return;
+ }
+
+ mInputUri = inputUri;
+ mFilename.setText(FileHelper.getFilename(getActivity(), mInputUri));
+ }
+
+ private void decryptAction() {
+ if (mInputUri == null) {
+ Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show();
+ return;
+ }
+
+ DecryptFilesActivity activity = (DecryptFilesActivity) getActivity();
+ activity.displayListFragment(mInputUri);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode != REQUEST_CODE_INPUT) {
+ return;
+ }
+
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ setInputUri(data.getData());
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesListFragment.java
new file mode 100644
index 000000000..d3b52fe78
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesListFragment.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnDismissListener;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.openintents.openpgp.OpenPgpMetadata;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
+// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15)
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
+import org.sufficientlysecure.keychain.ui.DecryptFilesListFragment.DecryptFilesAdapter.ViewModel;
+import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class DecryptFilesListFragment
+ extends CryptoOperationFragment<PgpDecryptVerifyInputParcel,DecryptVerifyResult>
+ implements OnMenuItemClickListener {
+ public static final String ARG_URIS = "uris";
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+
+ private ArrayList<Uri> mInputUris;
+ private HashMap<Uri, Uri> mOutputUris;
+ private ArrayList<Uri> mPendingInputUris;
+
+ private Uri mCurrentInputUri;
+
+ private DecryptFilesAdapter mAdapter;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static DecryptFilesListFragment newInstance(ArrayList<Uri> uris) {
+ DecryptFilesListFragment frag = new DecryptFilesListFragment();
+
+ Bundle args = new Bundle();
+ args.putParcelableArrayList(ARG_URIS, uris);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.decrypt_files_list_fragment, container, false);
+
+ RecyclerView vFilesList = (RecyclerView) view.findViewById(R.id.decrypted_files_list);
+
+ vFilesList.addItemDecoration(new SpacesItemDecoration(
+ FormattingUtils.dpToPx(getActivity(), 4)));
+ vFilesList.setHasFixedSize(true);
+ vFilesList.setLayoutManager(new LinearLayoutManager(getActivity()));
+ vFilesList.setItemAnimator(new DefaultItemAnimator());
+
+ mAdapter = new DecryptFilesAdapter(getActivity(), this);
+ vFilesList.setAdapter(mAdapter);
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putParcelableArrayList(ARG_URIS, mInputUris);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ displayInputUris(getArguments().<Uri>getParcelableArrayList(ARG_URIS));
+ }
+
+ private String removeEncryptedAppend(String name) {
+ if (name.endsWith(Constants.FILE_EXTENSION_ASC)
+ || name.endsWith(Constants.FILE_EXTENSION_PGP_MAIN)
+ || name.endsWith(Constants.FILE_EXTENSION_PGP_ALTERNATE)) {
+ return name.substring(0, name.length() - 4);
+ }
+ return name;
+ }
+
+ private void askForOutputFilename(Uri inputUri, String originalFilename, String mimeType) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ File file = new File(inputUri.getPath());
+ File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
+ File targetFile = new File(parentDir, originalFilename);
+ FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
+ getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
+ } else {
+ FileHelper.saveDocument(this, mimeType, originalFilename, REQUEST_CODE_OUTPUT);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_OUTPUT: {
+ // This happens after output file was selected, so start our operation
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ Uri saveUri = data.getData();
+ Uri outputUri = mOutputUris.get(mCurrentInputUri);
+ // TODO save from outputUri to saveUri
+
+ mCurrentInputUri = null;
+ }
+ return;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ private void displayInputUris(ArrayList<Uri> uris) {
+ mInputUris = uris;
+ mOutputUris = new HashMap<>(uris.size());
+ for (Uri uri : uris) {
+ mAdapter.add(uri);
+ mOutputUris.put(uri, TemporaryStorageProvider.createFile(getActivity()));
+ }
+
+ mPendingInputUris = uris;
+
+ cryptoOperation();
+ }
+
+ @Override
+ protected boolean onCryptoSetProgress(String msg, int progress, int max) {
+ mAdapter.setProgress(mCurrentInputUri, progress, max, msg);
+ return true;
+ }
+
+ @Override
+ protected void dismissProgress() {
+ // progress shown inline, so never mind
+ }
+
+ @Override
+ protected void onCryptoOperationError(DecryptVerifyResult result) {
+ final Uri uri = mCurrentInputUri;
+ mCurrentInputUri = null;
+
+ mAdapter.addResult(uri, result, null, null, null);
+ }
+
+ @Override
+ protected void onCryptoOperationSuccess(DecryptVerifyResult result) {
+ final Uri uri = mCurrentInputUri;
+ mCurrentInputUri = null;
+
+ Drawable icon = null;
+ OnClickListener onFileClick = null, onKeyClick = null;
+
+ if (result.getDecryptMetadata() != null && result.getDecryptMetadata().getMimeType() != null) {
+ icon = loadIcon(result.getDecryptMetadata().getMimeType());
+ }
+
+ OpenPgpSignatureResult sigResult = result.getSignatureResult();
+ if (sigResult != null) {
+ final long keyId = sigResult.getKeyId();
+ if (sigResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_KEY_MISSING) {
+ onKeyClick = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+ Intent intent = new Intent(activity, ViewKeyActivity.class);
+ intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId));
+ activity.startActivity(intent);
+ }
+ };
+ }
+ }
+
+ if (result.success() && result.getDecryptMetadata() != null) {
+ final OpenPgpMetadata metadata = result.getDecryptMetadata();
+ onFileClick = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Activity activity = getActivity();
+ if (activity == null || mCurrentInputUri != null) {
+ return;
+ }
+
+ Uri outputUri = mOutputUris.get(uri);
+ Intent intent = new Intent();
+ intent.setDataAndType(outputUri, metadata.getMimeType());
+ activity.startActivity(intent);
+ }
+ };
+ }
+
+ mAdapter.addResult(uri, result, icon, onFileClick, onKeyClick);
+
+ }
+
+ @Override
+ protected PgpDecryptVerifyInputParcel createOperationInput() {
+
+ if (mCurrentInputUri == null) {
+ if (mPendingInputUris.isEmpty()) {
+ // nothing left to do
+ return null;
+ }
+
+ mCurrentInputUri = mPendingInputUris.remove(0);
+ }
+
+ Uri currentOutputUri = mOutputUris.get(mCurrentInputUri);
+ Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri + ", mOutputUri=" + currentOutputUri);
+
+ return new PgpDecryptVerifyInputParcel(mCurrentInputUri, currentOutputUri)
+ .setAllowSymmetricDecryption(true);
+
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem menuItem) {
+ if (mAdapter.mMenuClickedModel == null || !mAdapter.mMenuClickedModel.hasResult()) {
+ return false;
+ }
+ Activity activity = getActivity();
+ if (activity == null) {
+ return false;
+ }
+
+ ViewModel model = mAdapter.mMenuClickedModel;
+ DecryptVerifyResult result = model.mResult;
+ switch (menuItem.getItemId()) {
+ case R.id.view_log:
+ Intent intent = new Intent(activity, LogDisplayActivity.class);
+ intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result);
+ activity.startActivity(intent);
+ return true;
+ case R.id.decrypt_save:
+ OpenPgpMetadata metadata = result.getDecryptMetadata();
+ if (metadata == null) {
+ return true;
+ }
+ mCurrentInputUri = model.mInputUri;
+ askForOutputFilename(model.mInputUri, metadata.getFilename(), metadata.getMimeType());
+ return true;
+ case R.id.decrypt_delete:
+ Notify.create(activity, "decrypt/delete not yet implemented", Style.ERROR).show(this);
+ return true;
+ }
+ return false;
+ }
+
+ public static class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
+ private Context mContext;
+ private ArrayList<ViewModel> mDataset;
+ private OnMenuItemClickListener mMenuItemClickListener;
+ private ViewModel mMenuClickedModel;
+
+ public class ViewModel {
+ Context mContext;
+ Uri mInputUri;
+ DecryptVerifyResult mResult;
+ Drawable mIcon;
+
+ OnClickListener mOnFileClickListener;
+ OnClickListener mOnKeyClickListener;
+
+ int mProgress, mMax;
+ String mProgressMsg;
+
+ ViewModel(Context context, Uri uri) {
+ mContext = context;
+ mInputUri = uri;
+ mProgress = 0;
+ mMax = 100;
+ }
+
+ void addResult(DecryptVerifyResult result) {
+ mResult = result;
+ }
+
+ void addIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ void setOnClickListeners(OnClickListener onFileClick, OnClickListener onKeyClick) {
+ mOnFileClickListener = onFileClick;
+ mOnKeyClickListener = onKeyClick;
+ }
+
+ boolean hasResult() {
+ return mResult != null;
+ }
+
+ void setProgress(int progress, int max, String msg) {
+ if (msg != null) {
+ mProgressMsg = msg;
+ }
+ mProgress = progress;
+ mMax = max;
+ }
+
+ // Depends on inputUri only
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ViewModel viewModel = (ViewModel) o;
+ return !(mResult != null ? !mResult.equals(viewModel.mResult)
+ : viewModel.mResult != null);
+ }
+
+ // Depends on inputUri only
+ @Override
+ public int hashCode() {
+ return mResult != null ? mResult.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return mResult.toString();
+ }
+ }
+
+ // Provide a suitable constructor (depends on the kind of dataset)
+ public DecryptFilesAdapter(Context context, OnMenuItemClickListener menuItemClickListener) {
+ mContext = context;
+ mMenuItemClickListener = menuItemClickListener;
+ mDataset = new ArrayList<>();
+ }
+
+ // Create new views (invoked by the layout manager)
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ //inflate your layout and pass it to view holder
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.decrypt_list_entry, parent, false);
+ return new ViewHolder(v);
+ }
+
+ // Replace the contents of a view (invoked by the layout manager)
+ @Override
+ public void onBindViewHolder(ViewHolder holder, final int position) {
+ // - get element from your dataset at this position
+ // - replace the contents of the view with that element
+ final ViewModel model = mDataset.get(position);
+
+ if (model.hasResult()) {
+ if (holder.vAnimator.getDisplayedChild() != 1) {
+ holder.vAnimator.setDisplayedChild(1);
+ }
+
+ KeyFormattingUtils.setStatus(mContext, holder, model.mResult);
+
+ OpenPgpMetadata metadata = model.mResult.getDecryptMetadata();
+ holder.vFilename.setText(metadata.getFilename());
+
+ long size = metadata.getOriginalSize();
+ if (size == -1 || size == 0) {
+ holder.vFilesize.setText("");
+ } else {
+ holder.vFilesize.setText(FileHelper.readableFileSize(size));
+ }
+
+ // TODO thumbnail from OpenPgpMetadata
+ if (model.mIcon != null) {
+ holder.vThumbnail.setImageDrawable(model.mIcon);
+ } else {
+ holder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am);
+ }
+
+ holder.vFile.setOnClickListener(model.mOnFileClickListener);
+ holder.vSignatureLayout.setOnClickListener(model.mOnKeyClickListener);
+
+ holder.vContextMenu.setTag(model);
+ holder.vContextMenu.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mMenuClickedModel = model;
+ PopupMenu menu = new PopupMenu(mContext, view);
+ menu.inflate(R.menu.decrypt_item_context_menu);
+ menu.setOnMenuItemClickListener(mMenuItemClickListener);
+ menu.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(PopupMenu popupMenu) {
+ mMenuClickedModel = null;
+ }
+ });
+ menu.show();
+ }
+ });
+
+ } else {
+ if (holder.vAnimator.getDisplayedChild() != 0) {
+ holder.vAnimator.setDisplayedChild(0);
+ }
+
+ holder.vProgress.setProgress(model.mProgress);
+ holder.vProgress.setMax(model.mMax);
+ holder.vProgressMsg.setText(model.mProgressMsg);
+ }
+
+ }
+
+ // Return the size of your dataset (invoked by the layout manager)
+ @Override
+ public int getItemCount() {
+ return mDataset.size();
+ }
+
+ public void add(Uri uri) {
+ ViewModel newModel = new ViewModel(mContext, uri);
+ mDataset.add(newModel);
+ notifyItemInserted(mDataset.size());
+ }
+
+ public void setProgress(Uri uri, int progress, int max, String msg) {
+ ViewModel newModel = new ViewModel(mContext, uri);
+ int pos = mDataset.indexOf(newModel);
+ mDataset.get(pos).setProgress(progress, max, msg);
+ notifyItemChanged(pos);
+ }
+
+ public void addResult(Uri uri, DecryptVerifyResult result, Drawable icon,
+ OnClickListener onFileClick, OnClickListener onKeyClick) {
+
+ ViewModel model = new ViewModel(mContext, uri);
+ int pos = mDataset.indexOf(model);
+ model = mDataset.get(pos);
+
+ model.addResult(result);
+ if (icon != null) {
+ model.addIcon(icon);
+ }
+ model.setOnClickListeners(onFileClick, onKeyClick);
+
+ notifyItemChanged(pos);
+ }
+
+ }
+
+
+ // Provide a reference to the views for each data item
+ // Complex data items may need more than one view per item, and
+ // you provide access to all the views for a data item in a view holder
+ public static class ViewHolder extends RecyclerView.ViewHolder implements StatusHolder {
+ public ViewAnimator vAnimator;
+
+ public ProgressBar vProgress;
+ public TextView vProgressMsg;
+
+ public View vFile;
+ public TextView vFilename;
+ public TextView vFilesize;
+ public ImageView vThumbnail;
+
+ public ImageView vEncStatusIcon;
+ public TextView vEncStatusText;
+
+ public ImageView vSigStatusIcon;
+ public TextView vSigStatusText;
+ public View vSignatureLayout;
+ public TextView vSignatureName;
+ public TextView vSignatureMail;
+ public TextView vSignatureAction;
+
+ public View vContextMenu;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+
+ vAnimator = (ViewAnimator) itemView.findViewById(R.id.view_animator);
+
+ vProgress = (ProgressBar) itemView.findViewById(R.id.progress);
+ vProgressMsg = (TextView) itemView.findViewById(R.id.progress_msg);
+
+ vFile = itemView.findViewById(R.id.file);
+ vFilename = (TextView) itemView.findViewById(R.id.filename);
+ vFilesize = (TextView) itemView.findViewById(R.id.filesize);
+ vThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
+
+ vEncStatusIcon = (ImageView) itemView.findViewById(R.id.result_encryption_icon);
+ vEncStatusText = (TextView) itemView.findViewById(R.id.result_encryption_text);
+
+ vSigStatusIcon = (ImageView) itemView.findViewById(R.id.result_signature_icon);
+ vSigStatusText = (TextView) itemView.findViewById(R.id.result_signature_text);
+ vSignatureLayout = itemView.findViewById(R.id.result_signature_layout);
+ vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name);
+ vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email);
+ vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action);
+
+ vContextMenu = itemView.findViewById(R.id.context_menu);
+
+ }
+
+ @Override
+ public ImageView getEncryptionStatusIcon() {
+ return vEncStatusIcon;
+ }
+
+ @Override
+ public TextView getEncryptionStatusText() {
+ return vEncStatusText;
+ }
+
+ @Override
+ public ImageView getSignatureStatusIcon() {
+ return vSigStatusIcon;
+ }
+
+ @Override
+ public TextView getSignatureStatusText() {
+ return vSigStatusText;
+ }
+
+ @Override
+ public View getSignatureLayout() {
+ return vSignatureLayout;
+ }
+
+ @Override
+ public TextView getSignatureAction() {
+ return vSignatureAction;
+ }
+
+ @Override
+ public TextView getSignatureUserName() {
+ return vSignatureName;
+ }
+
+ @Override
+ public TextView getSignatureUserEmail() {
+ return vSignatureMail;
+ }
+
+ @Override
+ public boolean hasEncrypt() {
+ return true;
+ }
+ }
+
+ private Drawable loadIcon(String mimeType) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setType(mimeType);
+
+ final List<ResolveInfo> matches = getActivity()
+ .getPackageManager().queryIntentActivities(intent, 0);
+ //noinspection LoopStatementThatDoesntLoop
+ for (ResolveInfo match : matches) {
+ return match.loadIcon(getActivity().getPackageManager());
+ }
+ return null;
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
index 2be162e95..5f1097588 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
@@ -155,6 +155,14 @@ public abstract class CryptoOperationFragment <T extends Parcelable, S extends O
onHandleResult(result);
}
}
+
+ @Override
+ protected void onSetProgress(String msg, int progress, int max) {
+ // allow handling of progress in fragment, or delegate upwards
+ if ( ! onCryptoSetProgress(msg, progress, max)) {
+ super.onSetProgress(msg, progress, max);
+ }
+ }
};
// Create a new Messenger for the communication back
@@ -213,5 +221,8 @@ public abstract class CryptoOperationFragment <T extends Parcelable, S extends O
}
+ protected boolean onCryptoSetProgress(String msg, int progress, int max) {
+ return false;
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
index dd85a6e46..11524aa08 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
@@ -24,9 +24,11 @@ import android.graphics.PorterDuff;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
+import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
+import org.openintents.openpgp.OpenPgpSignatureResult;
import org.spongycastle.asn1.ASN1ObjectIdentifier;
import org.spongycastle.asn1.nist.NISTNamedCurves;
import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves;
@@ -34,6 +36,8 @@ import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
import org.spongycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
+import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.util.Log;
@@ -376,7 +380,6 @@ public class KeyFormattingUtils {
/**
* Converts the given bytes to a unique RGB color using SHA1 algorithm
*
- * @param bytes
* @return an integer array containing 3 numeric color representations (Red, Green, Black)
* @throws java.security.NoSuchAlgorithmException
* @throws java.security.DigestException
@@ -394,7 +397,7 @@ public class KeyFormattingUtils {
public static final int DEFAULT_COLOR = -1;
- public static enum State {
+ public enum State {
REVOKED,
EXPIRED,
VERIFIED,
@@ -420,9 +423,165 @@ public class KeyFormattingUtils {
setStatusImage(context, statusIcon, statusText, state, color, false);
}
+ public interface StatusHolder {
+ ImageView getEncryptionStatusIcon();
+ TextView getEncryptionStatusText();
+
+ ImageView getSignatureStatusIcon();
+ TextView getSignatureStatusText();
+
+ View getSignatureLayout();
+ TextView getSignatureUserName();
+ TextView getSignatureUserEmail();
+ TextView getSignatureAction();
+
+ boolean hasEncrypt();
+
+ }
+
+ @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated
+ public static void setStatus(Context context, StatusHolder holder, DecryptVerifyResult result) {
+
+ OpenPgpSignatureResult signatureResult = result.getSignatureResult();
+
+ if (holder.hasEncrypt()) {
+ int encText, encIcon, encColor;
+ if (signatureResult != null && signatureResult.isSignatureOnly()) {
+ encIcon = R.drawable.status_lock_open_24dp;
+ encText = R.string.decrypt_result_not_encrypted;
+ encColor = R.color.android_red_light;
+ } else {
+ encIcon = R.drawable.status_lock_closed_24dp;
+ encText = R.string.decrypt_result_encrypted;
+ encColor = R.color.android_green_light;
+ }
+
+ int encColorRes = context.getResources().getColor(encColor);
+ holder.getEncryptionStatusIcon().setImageDrawable(context.getResources().getDrawable(encIcon));
+ holder.getEncryptionStatusIcon().setColorFilter(encColorRes, PorterDuff.Mode.SRC_IN);
+ holder.getEncryptionStatusText().setText(encText);
+ holder.getEncryptionStatusText().setTextColor(encColorRes);
+ }
+
+ int sigText, sigIcon, sigColor;
+ int sigActionText, sigActionIcon;
+
+ if (signatureResult == null) {
+
+ sigText = R.string.decrypt_result_no_signature;
+ sigIcon = R.drawable.status_signature_invalid_cutout_24dp;
+ sigColor = R.color.bg_gray;
+
+ // won't be used, but makes compiler happy
+ sigActionText = 0;
+ sigActionIcon = 0;
+
+ } else switch (signatureResult.getStatus()) {
+
+ case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: {
+ sigText = R.string.decrypt_result_signature_certified;
+ sigIcon = R.drawable.status_signature_verified_cutout_24dp;
+ sigColor = R.color.android_green_light;
+
+ sigActionText = R.string.decrypt_result_action_show;
+ sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ break;
+ }
+
+ case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED: {
+ sigText = R.string.decrypt_result_signature_uncertified;
+ sigIcon = R.drawable.status_signature_unverified_cutout_24dp;
+ sigColor = R.color.android_orange_light;
+
+ sigActionText = R.string.decrypt_result_action_show;
+ sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ break;
+ }
+
+ case OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED: {
+ sigText = R.string.decrypt_result_signature_revoked_key;
+ sigIcon = R.drawable.status_signature_revoked_cutout_24dp;
+ sigColor = R.color.android_red_light;
+
+ sigActionText = R.string.decrypt_result_action_show;
+ sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ break;
+ }
+
+ case OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED: {
+ sigText = R.string.decrypt_result_signature_expired_key;
+ sigIcon = R.drawable.status_signature_expired_cutout_24dp;
+ sigColor = R.color.android_red_light;
+
+ sigActionText = R.string.decrypt_result_action_show;
+ sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ break;
+ }
+
+ case OpenPgpSignatureResult.SIGNATURE_KEY_MISSING: {
+ sigText = R.string.decrypt_result_signature_missing_key;
+ sigIcon = R.drawable.status_signature_unknown_cutout_24dp;
+ sigColor = R.color.android_red_light;
+
+ sigActionText = R.string.decrypt_result_action_Lookup;
+ sigActionIcon = R.drawable.ic_file_download_grey_24dp;
+ break;
+ }
+
+ default:
+ case OpenPgpSignatureResult.SIGNATURE_ERROR: {
+ sigText = R.string.decrypt_result_invalid_signature;
+ sigIcon = R.drawable.status_signature_invalid_cutout_24dp;
+ sigColor = R.color.android_red_light;
+
+ sigActionText = R.string.decrypt_result_action_show;
+ sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ break;
+ }
+
+ }
+
+ int sigColorRes = context.getResources().getColor(sigColor);
+ holder.getSignatureStatusIcon().setImageDrawable(context.getResources().getDrawable(sigIcon));
+ holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN);
+ holder.getSignatureStatusText().setText(sigText);
+ holder.getSignatureStatusText().setTextColor(sigColorRes);
+
+ if (signatureResult != null) {
+
+ holder.getSignatureLayout().setVisibility(View.VISIBLE);
+
+ holder.getSignatureAction().setText(sigActionText);
+ holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds(
+ 0, 0, sigActionIcon, 0);
+
+ String userId = signatureResult.getPrimaryUserId();
+ KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId);
+ if (userIdSplit.name != null) {
+ holder.getSignatureUserName().setText(userIdSplit.name);
+ } else {
+ holder.getSignatureUserName().setText(R.string.user_id_no_name);
+ }
+ if (userIdSplit.email != null) {
+ holder.getSignatureUserEmail().setVisibility(View.VISIBLE);
+ holder.getSignatureUserEmail().setText(userIdSplit.email);
+ } else {
+ holder.getSignatureUserEmail().setVisibility(View.GONE);
+ }
+
+ } else {
+
+ holder.getSignatureLayout().setVisibility(View.GONE);
+
+ }
+
+
+ }
+
/**
* Sets status image based on constant
*/
+ @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21
public static void setStatusImage(Context context, ImageView statusIcon, TextView statusText,
State state, int color, boolean big) {
switch (state) {
diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml
deleted file mode 100644
index 22ee7e09f..000000000
--- a/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml
+++ /dev/null
@@ -1,149 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <ScrollView
- android:fillViewport="true"
- android:paddingTop="8dp"
- android:layout_width="match_parent"
- android:scrollbars="vertical"
- android:layout_height="0dp"
- android:layout_weight="1">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <View
- android:id="@+id/status_divider"
- android:layout_height="1dip"
- android:layout_width="match_parent"
- android:background="?android:attr/listDivider" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingTop="4dp"
- android:paddingLeft="16dp"
- android:paddingRight="16dp"
- android:orientation="vertical">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
- android:orientation="horizontal"
-
- android:id="@+id/decrypt_files_browse"
- android:clickable="true"
- android:background="?android:selectableItemBackground">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:paddingLeft="8dp"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:text="@string/label_file_colon"
- android:gravity="center_vertical" />
-
- <TextView
- android:id="@+id/decrypt_files_filename"
- android:paddingLeft="8dp"
- android:paddingRight="8dp"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:hint="@string/filemanager_title_open"
- android:drawableRight="@drawable/ic_folder_grey_24dp"
- android:drawablePadding="8dp"
- android:gravity="center_vertical" />
- </LinearLayout>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dip"
- android:background="?android:attr/listDivider"
- android:layout_marginBottom="8dp" />
-
- <CheckBox
- android:id="@+id/decrypt_files_delete_after_decryption"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/label_delete_after_decryption" />
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:id="@+id/decrypt_files_action_decrypt"
- android:paddingLeft="8dp"
- android:paddingRight="8dp"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:text="@string/btn_decrypt_verify_file"
- android:clickable="true"
- android:background="?android:selectableItemBackground"
- android:drawableRight="@drawable/ic_save_grey_24dp"
- android:drawablePadding="8dp"
- android:gravity="center_vertical"
- android:layout_alignParentBottom="true"
- android:layout_alignParentLeft="true"
- android:layout_alignParentStart="true" />
-
- <View
- android:layout_width="match_parent"
- android:layout_height="1dip"
- android:background="?android:attr/listDivider"
- android:layout_above="@+id/decrypt_files_action_decrypt" />
-
- </RelativeLayout>
- </LinearLayout>
- </LinearLayout>
- </ScrollView>
- </LinearLayout>
-
- <!-- TODO: Use this layout later to hide file list -->
- <LinearLayout
- android:visibility="gone"
- android:id="@+id/decrypt_content"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"></LinearLayout>
-
- <LinearLayout
- android:visibility="gone"
- android:id="@+id/decrypt_error_overlay"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:gravity="center_vertical">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:text="@string/decrypt_invalid_text"
- android:padding="16dp"
- android:layout_gravity="center"
- android:textColor="@color/android_red_light" />
-
- <Button
- android:id="@+id/decrypt_error_overlay_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/button_edgy"
- android:textColor="@color/android_red_light"
- android:text="@string/decrypt_invalid_button"
- android:layout_gravity="center_horizontal" />
- </LinearLayout>
-</LinearLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml
new file mode 100644
index 000000000..b7e70ce10
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="4dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/status_divider"
+ android:layout_height="1dip"
+ android:layout_width="match_parent"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:id="@+id/decrypt_files_browse"
+ android:clickable="true"
+ android:background="?android:selectableItemBackground">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/label_file_colon"
+ android:gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/decrypt_files_filename"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:hint="@string/filemanager_title_open"
+ android:drawableRight="@drawable/ic_folder_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"
+ android:layout_marginBottom="8dp" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/decrypt_files_action_decrypt"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:text="@string/btn_decrypt_verify_file"
+ android:clickable="true"
+ android:background="?android:selectableItemBackground"
+ android:drawableRight="@drawable/ic_save_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"
+ android:layout_above="@+id/decrypt_files_action_decrypt" />
+
+ </RelativeLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml
new file mode 100644
index 000000000..8cee4e77a
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/decrypt_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/decrypted_files_list"
+ android:paddingTop="16dp"
+ android:scrollbars="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:visibility="gone"
+ android:id="@+id/decrypt_error_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/decrypt_invalid_text"
+ android:padding="16dp"
+ android:layout_gravity="center"
+ android:textColor="@color/android_red_light" />
+
+ <Button
+ android:id="@+id/decrypt_error_overlay_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/button_edgy"
+ android:textColor="@color/android_red_light"
+ android:text="@string/decrypt_invalid_button"
+ android:layout_gravity="center_horizontal" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
new file mode 100644
index 000000000..862258152
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ custom:cardBackgroundColor="@android:color/white"
+ custom:cardElevation="2dp"
+ custom:cardUseCompatPadding="true"
+ custom:cardCornerRadius="4dp"
+ >
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:paddingRight="8dp"
+ android:paddingLeft="8dp"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:id="@+id/view_animator"
+ custom:initialView="1"
+ >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+
+ <ProgressBar
+ android:id="@+id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:indeterminate="false"
+ style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+ android:progress="40"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/progress_msg"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text=""
+ tools:text="Progress Message"
+ android:layout_gravity="center_vertical" />
+
+ </LinearLayout>
+ <!-- -->
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingRight="4dp"
+ android:paddingLeft="4dp"
+ tools:ignore="UseCompoundDrawables">
+
+ <ImageView
+ android:id="@+id/result_encryption_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/status_lock_open_24dp"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/result_encryption_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:text=""
+ tools:text="Encryption status text" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingRight="4dp"
+ android:paddingLeft="4dp"
+ tools:ignore="UseCompoundDrawables">
+
+ <ImageView
+ android:id="@+id/result_signature_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/status_signature_unverified_cutout_24dp"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/result_signature_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:text=""
+ tools:text="Signature status text" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/result_signature_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingRight="4dp"
+ android:paddingLeft="4dp"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/result_signature_name"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ tools:text="Alice" />
+
+ <TextView
+ android:id="@+id/result_signature_email"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:text=""
+ tools:text="alice@example.com" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="1dip"
+ android:layout_height="match_parent"
+ android:gravity="right"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/result_signature_action"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:drawableRight="@drawable/ic_vpn_key_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:text=""
+ tools:text="Show"
+ />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ tools:layout_height="2dip"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/file"
+ android:clickable="true"
+ android:background="?android:selectableItemBackground"
+ >
+
+ <ImageView
+ android:id="@+id/thumbnail"
+ android:layout_gravity="center_vertical"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:scaleType="center"
+ android:padding="6dp"
+ android:src="@drawable/ic_doc_generic_am" />
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/filename"
+ android:maxLines="1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:ellipsize="end"
+ android:text=""
+ tools:text="filename.jpg" />
+
+ <TextView
+ android:id="@+id/filesize"
+ android:maxLines="1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:ellipsize="end"
+ android:text=""
+ tools:text="14kb" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/context_menu"
+ android:scaleType="center"
+ android:layout_width="36dip"
+ android:layout_height="48dip"
+ android:clickable="true"
+ android:background="?android:selectableItemBackground"
+ android:src="@drawable/abc_ic_menu_moreoverflow_mtrl_alpha" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml b/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml
new file mode 100644
index 000000000..e18bdf8e4
--- /dev/null
+++ b/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/view_log"
+ android:title="@string/view_log"
+ android:icon="@drawable/ic_view_list_grey_24dp"
+ />
+
+ <item
+ android:id="@+id/decrypt_save"
+ android:title="@string/btn_save"
+ android:icon="@drawable/ic_action_encrypt_file_24dp"
+ />
+
+ <item
+ android:id="@+id/decrypt_delete"
+ android:title="@string/btn_delete_original"
+ android:icon="@drawable/ic_delete_grey_24dp"
+ />
+
+</menu> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index e8d1f1e70..ae131e029 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -1323,6 +1323,8 @@
<string name="error_nfc_header">"NFC: Card reported invalid %s byte"</string>
<string name="error_pin_nodefault">Default PIN was rejected!</string>
<string name="error_bluetooth_file">Error creating temporary file. Bluetooth sharing will fail.</string>
+ <string name="btn_delete_original">Delete original file</string>
+
<string name="snack_encrypt_filenames_on">"Filenames <b>are</b> encrypted."</string>
<string name="snack_encrypt_filenames_off">"Filenames <b>are not</b> encrypted."</string>
<string name="snack_armor_on">"Output encoded as Text."</string>
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
index ce7414a3d..9c3636d07 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
@@ -171,6 +171,10 @@ public class PgpEncryptDecryptTest {
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature should be an error", result.getSignatureResult());
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("cached session keys must be empty",
+ 0, cryptoInput.getCryptoData().size());
+
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
@@ -272,6 +276,10 @@ public class PgpEncryptDecryptTest {
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature be empty", result.getSignatureResult());
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("must have one cached session key",
+ 1, cryptoInput.getCryptoData().size());
+
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
@@ -289,6 +297,10 @@ public class PgpEncryptDecryptTest {
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("must have one cached session key",
+ 1, cryptoInput.getCryptoData().size());
+
Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());