aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2015-11-19 14:00:17 +0100
committerDominik Schürmann <dominik@dominikschuermann.de>2015-11-19 14:00:17 +0100
commit2fe508c60e51693b383481d6e626ccada85313f5 (patch)
tree9182ab2a5ecf272c02659bd5bd2dc999470e6b2a
parent62918c571899855bc4a16a77c9f423ff24c7d036 (diff)
parentd4c121c2aff58fc193c4d715f4475d388b6de91e (diff)
downloadopen-keychain-2fe508c60e51693b383481d6e626ccada85313f5.tar.gz
open-keychain-2fe508c60e51693b383481d6e626ccada85313f5.tar.bz2
open-keychain-2fe508c60e51693b383481d6e626ccada85313f5.zip
Merge branch 'master' of github.com:open-keychain/open-keychain
-rw-r--r--.travis.yml2
-rw-r--r--OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java16
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java26
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java47
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java360
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java10
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java101
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java41
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java56
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_list_entry.xml55
-rw-r--r--OpenKeychain/src/main/res/layout/key_list_item.xml3
-rw-r--r--OpenKeychain/src/main/res/layout/view_key_activity.xml4
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml7
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java19
-rw-r--r--README.md1
21 files changed, 594 insertions, 252 deletions
diff --git a/.travis.yml b/.travis.yml
index 74ea7a58d..145d87862 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,4 +35,4 @@ android:
script:
# - ./gradlew connectedAndroidTest
- - ./gradlew testDebug jacocoTestReport coveralls
+ - ./gradlew --stacktrace testDebug jacocoTestReport coveralls
diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java
new file mode 100644
index 000000000..f1cf9791a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java
@@ -0,0 +1,32 @@
+package org.spongycastle.openpgp.jcajce;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.spongycastle.openpgp.PGPMarker;
+
+/** This class wraps the regular PGPObjectFactory, changing its behavior to
+ * ignore all PGPMarker packets it encounters while reading. These packets
+ * carry no semantics of their own, and should be ignored according to
+ * RFC 4880.
+ *
+ * @see https://tools.ietf.org/html/rfc4880#section-5.8
+ * @see org.spongycastle.openpgp.PGPMarker
+ *
+ */
+public class JcaSkipMarkerPGPObjectFactory extends JcaPGPObjectFactory {
+
+ public JcaSkipMarkerPGPObjectFactory(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public Object nextObject() throws IOException {
+ Object o = super.nextObject();
+ while (o instanceof PGPMarker) {
+ o = super.nextObject();
+ }
+ return o;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
index 79175e9ad..69f1862ce 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
@@ -28,6 +28,7 @@ public final class Constants {
public static final boolean DEBUG = BuildConfig.DEBUG;
public static final boolean DEBUG_LOG_DB_QUERIES = false;
+ public static final boolean DEBUG_EXPLAIN_QUERIES = false;
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
public static final boolean DEBUG_KEYSERVER_SYNC = false;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
index a09cf4f27..bb8d6ad73 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
@@ -110,10 +110,9 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
if (decryptResult.isPending()) {
return new InputDataResult(log, decryptResult);
}
- log.addByMerge(decryptResult, 2);
+ log.addByMerge(decryptResult, 1);
- if (!decryptResult.success()) {
- log.add(LogType.MSG_DATA_ERROR_OPENPGP, 1);
+ if ( ! decryptResult.success()) {
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
}
@@ -165,6 +164,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
parser.setContentDecoding(true);
parser.setRecurse();
parser.setContentHandler(new AbstractContentHandler() {
+ private boolean mFoundHeaderWithFields = false;
private Uri uncheckedSignedDataUri;
String mFilename;
@@ -221,11 +221,19 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
}
@Override
+ public void endHeader() throws MimeException {
+ if ( ! mFoundHeaderWithFields) {
+ parser.stop();
+ }
+ }
+
+ @Override
public void field(Field field) throws MimeException {
field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT);
if (field instanceof ContentDispositionField) {
mFilename = ((ContentDispositionField) field).getFilename();
}
+ mFoundHeaderWithFields = true;
}
private void bodySignature(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
index 0f9f45cbf..9877f2318 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
@@ -836,7 +836,6 @@ public abstract class OperationResult implements Parcelable {
MSG_DATA (LogLevel.START, R.string.msg_data),
MSG_DATA_OPENPGP (LogLevel.DEBUG, R.string.msg_data_openpgp),
MSG_DATA_ERROR_IO (LogLevel.ERROR, R.string.msg_data_error_io),
- MSG_DATA_ERROR_OPENPGP (LogLevel.ERROR, R.string.msg_data_error_openpgp),
MSG_DATA_DETACHED (LogLevel.INFO, R.string.msg_data_detached),
MSG_DATA_DETACHED_CLEAR (LogLevel.WARN, R.string.msg_data_detached_clear),
MSG_DATA_DETACHED_SIG (LogLevel.DEBUG, R.string.msg_data_detached_sig),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
index 1511fd5b1..ea7465209 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
@@ -48,7 +48,7 @@ import org.spongycastle.openpgp.PGPPBEEncryptedData;
import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
import org.spongycastle.openpgp.PGPSignatureList;
import org.spongycastle.openpgp.PGPUtil;
-import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.spongycastle.openpgp.jcajce.JcaSkipMarkerPGPObjectFactory;
import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory;
@@ -281,11 +281,11 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
- JcaPGPObjectFactory plainFact;
+ JcaSkipMarkerPGPObjectFactory plainFact;
Object dataChunk;
EncryptStreamResult esResult = null;
{ // resolve encrypted (symmetric and asymmetric) packets
- JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
+ JcaSkipMarkerPGPObjectFactory pgpF = new JcaSkipMarkerPGPObjectFactory(in);
Object obj = pgpF.nextObject();
if (obj instanceof PGPEncryptedDataList) {
@@ -312,7 +312,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
decryptionResultBuilder.setInsecure(true);
}
- plainFact = new JcaPGPObjectFactory(esResult.cleartextStream);
+ plainFact = new JcaSkipMarkerPGPObjectFactory(esResult.cleartextStream);
dataChunk = plainFact.nextObject();
} else {
@@ -337,7 +337,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
PGPCompressedData compressedData = (PGPCompressedData) dataChunk;
- JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream());
+ JcaSkipMarkerPGPObjectFactory fact = new JcaSkipMarkerPGPObjectFactory(compressedData.getDataStream());
dataChunk = fact.nextObject();
plainFact = fact;
}
@@ -839,7 +839,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
}
updateProgress(R.string.progress_processing_signature, 60, 100);
- JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(aIn);
+ JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(aIn);
PgpSignatureChecker signatureChecker = new PgpSignatureChecker(mProviderHelper);
@@ -891,12 +891,12 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
InputStream detachedSigIn = new ByteArrayInputStream(input.getDetachedSignature());
detachedSigIn = PGPUtil.getDecoderStream(detachedSigIn);
- JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(detachedSigIn);
+ JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(detachedSigIn);
Object o = pgpFact.nextObject();
if (o instanceof PGPCompressedData) {
PGPCompressedData c1 = (PGPCompressedData) o;
- pgpFact = new JcaPGPObjectFactory(c1.getDataStream());
+ pgpFact = new JcaSkipMarkerPGPObjectFactory(c1.getDataStream());
o = pgpFact.nextObject();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
index fbda90775..016651c3b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
@@ -18,23 +18,15 @@
package org.sufficientlysecure.keychain.pgp;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
-import org.sufficientlysecure.keychain.util.Preferences;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.security.SecureRandom;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
public class PgpHelper {
@@ -51,35 +43,6 @@ public class PgpHelper {
Pattern.DOTALL);
/**
- * Deletes file securely by overwriting it with random data before deleting it.
- * <p/>
- * TODO: Does this really help on flash storage?
- *
- * @throws IOException
- */
- public static void deleteFileSecurely(Context context, Progressable progressable, File file)
- throws IOException {
- long length = file.length();
- SecureRandom random = new SecureRandom();
- RandomAccessFile raf = new RandomAccessFile(file, "rws");
- raf.seek(0);
- raf.getFilePointer();
- byte[] data = new byte[1 << 16];
- int pos = 0;
- String msg = context.getString(R.string.progress_deleting_securely, file.getName());
- while (pos < length) {
- if (progressable != null) {
- progressable.setProgress(msg, (int) (100 * pos / length), 100);
- }
- random.nextBytes(data);
- raf.write(data);
- pos += data.length;
- }
- raf.close();
- file.delete();
- }
-
- /**
* Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail
*/
public static String fixPgpMessage(String message) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
index 0f90f8141..752c13007 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
@@ -54,7 +54,7 @@ import java.io.IOException;
*/
public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db";
- private static final int DATABASE_VERSION = 13;
+ private static final int DATABASE_VERSION = 14;
static Boolean apgHack = false;
private Context mContext;
@@ -79,7 +79,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
private static final String CREATE_KEYRINGS_SECRET =
"CREATE TABLE IF NOT EXISTS keyrings_secret ("
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
- + KeyRingsColumns.KEY_RING_DATA + " BLOB,"
+ + KeyRingsColumns.KEY_RING_DATA + " BLOB, "
+ "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") "
+ "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
+ ")";
@@ -220,6 +220,13 @@ public class KeychainDatabase extends SQLiteOpenHelper {
db.execSQL(CREATE_API_APPS);
db.execSQL(CREATE_API_APPS_ACCOUNTS);
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
+
+ db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
+ db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ + UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
+ db.execSQL("CREATE INDEX verified_certs ON certs ("
+ + CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
+
}
@Override
@@ -291,13 +298,14 @@ public class KeychainDatabase extends SQLiteOpenHelper {
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
case 12:
db.execSQL(CREATE_UPDATE_KEYS);
- if (oldVersion == 10) {
- // no consolidate if we are updating from 10, we're just here for
- // the api_accounts fix and the new update keys table
- return;
- }
case 13:
// do nothing here, just consolidate
+ case 14:
+ db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
+ db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ + UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
+ db.execSQL("CREATE INDEX verified_certs ON certs ("
+ + CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
}
@@ -310,6 +318,10 @@ public class KeychainDatabase extends SQLiteOpenHelper {
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // Downgrade is ok for the debug version, makes it easier to work with branches
+ if (Constants.DEBUG) {
+ return;
+ }
// NOTE: downgrading the database is explicitly not allowed to prevent
// someone from exploiting old bugs to export the database
throw new RuntimeException("Downgrading the database is not allowed!");
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
index d722fa9e7..104343074 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -303,14 +303,14 @@ public class KeychainProvider extends ContentProvider {
projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT);
projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID);
projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID,
- "(SELECT COUNT (*) FROM " + Tables.USER_PACKETS + " AS dups"
+ "(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
+ " WHERE dups." + UserPackets.MASTER_KEY_ID
+ " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ " AND dups." + UserPackets.RANK + " = 0"
+ " AND dups." + UserPackets.USER_ID
+ " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID
- + ") AS " + KeyRings.HAS_DUPLICATE_USER_ID);
- projectionMap.put(KeyRings.VERIFIED, KeyRings.VERIFIED);
+ + ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID);
+ projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
projectionMap.put(KeyRings.PUBKEY_DATA,
Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.KEY_RING_DATA
+ " AS " + KeyRings.PUBKEY_DATA);
@@ -319,10 +319,8 @@ public class KeychainProvider extends ContentProvider {
+ " AS " + KeyRings.PRIVKEY_DATA);
projectionMap.put(KeyRings.HAS_SECRET, Tables.KEYS + "." + KeyRings.HAS_SECRET);
projectionMap.put(KeyRings.HAS_ANY_SECRET,
- "(EXISTS (SELECT * FROM " + Tables.KEY_RINGS_SECRET
- + " WHERE " + Tables.KEY_RINGS_SECRET + "." + KeyRingData.MASTER_KEY_ID
- + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
- + ")) AS " + KeyRings.HAS_ANY_SECRET);
+ "(" + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL)" +
+ " AS " + KeyRings.HAS_ANY_SECRET);
projectionMap.put(KeyRings.HAS_ENCRYPT,
"kE." + Keys.KEY_ID + " AS " + KeyRings.HAS_ENCRYPT);
projectionMap.put(KeyRings.HAS_SIGN,
@@ -363,7 +361,7 @@ public class KeychainProvider extends ContentProvider {
+ " = "
+ Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.MASTER_KEY_ID
+ ")" : "")
- + (plist.contains(KeyRings.PRIVKEY_DATA) ?
+ + (plist.contains(KeyRings.PRIVKEY_DATA) || plist.contains(KeyRings.HAS_ANY_SECRET) ?
" LEFT JOIN " + Tables.KEY_RINGS_SECRET + " ON ("
+ Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ " = "
@@ -712,20 +710,45 @@ public class KeychainProvider extends ContentProvider {
}
SQLiteDatabase db = getDb().getReadableDatabase();
+
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
if (cursor != null) {
// Tell the cursor what uri to watch, so it knows when its source data changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
+ Log.d(Constants.TAG,
+ "Query: " + qb.buildQuery(projection, selection, null, null, orderBy, null));
+
if (Constants.DEBUG && Constants.DEBUG_LOG_DB_QUERIES) {
- Log.d(Constants.TAG,
- "Query: "
- + qb.buildQuery(projection, selection, selectionArgs, null, null,
- orderBy, null));
Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(cursor));
}
+ if (Constants.DEBUG && Constants.DEBUG_EXPLAIN_QUERIES) {
+ String rawQuery = qb.buildQuery(projection, selection, groupBy, having, orderBy, null);
+ Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + rawQuery, selectionArgs);
+
+ // this is a debugging feature, we can be a little careless
+ explainCursor.moveToFirst();
+
+ StringBuilder line = new StringBuilder();
+ for (int i = 0; i < explainCursor.getColumnCount(); i++) {
+ line.append(explainCursor.getColumnName(i)).append(", ");
+ }
+ Log.d(Constants.TAG, line.toString());
+
+ while (!explainCursor.isAfterLast()) {
+ line = new StringBuilder();
+ for (int i = 0; i < explainCursor.getColumnCount(); i++) {
+ line.append(explainCursor.getString(i)).append(", ");
+ }
+ Log.d(Constants.TAG, line.toString());
+ explainCursor.moveToNext();
+ }
+
+ explainCursor.close();
+ }
+
return cursor;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
index dbee564b1..8adaa0670 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
@@ -73,10 +73,14 @@ import org.openintents.openpgp.OpenPgpSignatureResult;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.InputDataResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.InputDataParcel;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
// this import NEEDS to be above the ViewModel AND SubViewHolder one, or it won't compile! (as of 16.09.15)
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
@@ -90,8 +94,25 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableHashMap;
+import org.sufficientlysecure.keychain.util.Preferences;
+/** Displays a list of decrypted inputs.
+ *
+ * This class has a complex control flow to manage its input URIs. Each URI
+ * which is in mInputUris is also in exactly one of mPendingInputUris,
+ * mCancelledInputUris, mCurrentInputUri, or a key in mInputDataResults.
+ *
+ * Processing of URIs happens using a looping approach:
+ * - There is always exactly one method running which works on mCurrentInputUri
+ * - Processing starts in cryptoOperation(), which pops a new mCurrentInputUri
+ * from the list of mPendingInputUris.
+ * - Once a mCurrentInputUri is finished processing, it should be set to null and
+ * control handed back to cryptoOperation()
+ * - Control flow can move through asynchronous calls, and resume in callbacks
+ * like onActivityResult() or onPermissionRequestResult().
+ *
+ */
public class DecryptListFragment
extends QueueingCryptoOperationFragment<InputDataParcel,InputDataResult>
implements OnMenuItemClickListener {
@@ -103,7 +124,7 @@ public class DecryptListFragment
public static final String ARG_CAN_DELETE = "can_delete";
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
- private static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 12;
+ private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;
private ArrayList<Uri> mInputUris;
private HashMap<Uri, InputDataResult> mInputDataResults;
@@ -201,7 +222,9 @@ public class DecryptListFragment
);
}
- private void displayInputUris(ArrayList<Uri> inputUris, ArrayList<Uri> cancelledUris,
+ private void displayInputUris(
+ ArrayList<Uri> inputUris,
+ ArrayList<Uri> cancelledUris,
HashMap<Uri,InputDataResult> results) {
mInputUris = inputUris;
@@ -214,21 +237,19 @@ public class DecryptListFragment
for (final Uri uri : inputUris) {
mAdapter.add(uri);
- if (mCancelledInputUris.contains(uri)) {
- mAdapter.setCancelled(uri, new OnClickListener() {
- @Override
- public void onClick(View v) {
- retryUri(uri);
- }
- });
+ boolean uriIsCancelled = mCancelledInputUris.contains(uri);
+ if (uriIsCancelled) {
+ mAdapter.setCancelled(uri, true);
continue;
}
- if (results != null && results.containsKey(uri)) {
+ boolean uriHasResult = results != null && results.containsKey(uri);
+ if (uriHasResult) {
processResult(uri);
- } else {
- mPendingInputUris.add(uri);
+ continue;
}
+
+ mPendingInputUris.add(uri);
}
// check if there are any pending input uris
@@ -362,12 +383,7 @@ public class DecryptListFragment
mCurrentInputUri = null;
mCancelledInputUris.add(uri);
- mAdapter.setCancelled(uri, new OnClickListener() {
- @Override
- public void onClick(View v) {
- retryUri(uri);
- }
- });
+ mAdapter.setCancelled(uri, true);
cryptoOperation();
@@ -457,8 +473,9 @@ public class DecryptListFragment
// un-cancel this one
mCancelledInputUris.remove(uri);
+ mInputDataResults.remove(uri);
mPendingInputUris.add(uri);
- mAdapter.setCancelled(uri, null);
+ mAdapter.resetItemData(uri);
// check if there are any pending input uris
cryptoOperation();
@@ -582,6 +599,11 @@ public class DecryptListFragment
@Override
public InputDataParcel createOperationInput() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return null;
+ }
+
if (mCurrentInputUri == null) {
if (mPendingInputUris.isEmpty()) {
// nothing left to do
@@ -593,95 +615,102 @@ public class DecryptListFragment
Log.d(Constants.TAG, "mCurrentInputUri=" + mCurrentInputUri);
- if (readPermissionGranted(mCurrentInputUri)) {
- PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
- .setAllowSymmetricDecryption(true);
- return new InputDataParcel(mCurrentInputUri, decryptInput);
- } else {
+ if ( ! checkAndRequestReadPermission(activity, mCurrentInputUri)) {
return null;
}
+
+ PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
+ .setAllowSymmetricDecryption(true);
+ return new InputDataParcel(mCurrentInputUri, decryptInput);
+
}
/**
- * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris
+ * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
+ *
+ * This method returns true on Android < 6, or if permission is already granted. It
+ * requests the permission and returns false otherwise, taking over responsibility
+ * for mCurrentInputUri.
*
- * see
- * https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
+ * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
*/
- private boolean readPermissionGranted(final Uri uri) {
+ private boolean checkAndRequestReadPermission(Activity activity, final Uri uri) {
+ if ( ! "file".equals(uri.getScheme())) {
+ return true;
+ }
+
if (Build.VERSION.SDK_INT < VERSION_CODES.M) {
return true;
}
- if (! "file".equals(uri.getScheme())) {
+
+ // Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return true;
}
- // Build check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN ||
- ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
return true;
- } else {
- requestPermissions(
- new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
- MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
-
- mCurrentInputUri = null;
- mCancelledInputUris.add(uri);
- mAdapter.setCancelled(uri, new OnClickListener() {
- @Override
- public void onClick(View v) {
- retryUri(uri);
- }
- });
- return false;
}
+
+ requestPermissions(
+ new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
+ REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);
+
+ return false;
+
}
@Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults) {
- switch (requestCode) {
- case MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-
- // permission granted -> retry all cancelled file uris!
- for (Iterator<Uri> iterator = mCancelledInputUris.iterator(); iterator.hasNext(); ) {
- Uri uri = iterator.next();
-
- if ("file".equals(uri.getScheme())) {
- iterator.remove();
- mPendingInputUris.add(uri);
- mAdapter.setCancelled(uri, null);
- }
- }
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
- // check if there are any pending input uris
- cryptoOperation();
- } else {
+ if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ return;
+ }
- // permission denied -> cancel all pending file uris
- mCurrentInputUri = null;
- for (final Uri uri : mPendingInputUris) {
- if ("file".equals(uri.getScheme())) {
- if (! mCancelledInputUris.contains(uri)) {
- mCancelledInputUris.add(uri);
- }
- mAdapter.setCancelled(uri, new OnClickListener() {
- @Override
- public void onClick(View v) {
- retryUri(uri);
- }
- });
- }
- }
+ boolean permissionWasGranted = grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+
+ if (permissionWasGranted) {
+
+ // permission granted -> retry all cancelled file uris
+ Iterator<Uri> it = mCancelledInputUris.iterator();
+ while (it.hasNext()) {
+ Uri uri = it.next();
+ if ( ! "file".equals(uri.getScheme())) {
+ continue;
}
+ it.remove();
+ mPendingInputUris.add(uri);
+ mAdapter.setCancelled(uri, false);
}
- default: {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ } else {
+
+ // permission denied -> cancel current, and all pending file uris
+ mCancelledInputUris.add(mCurrentInputUri);
+ mAdapter.setCancelled(mCurrentInputUri, true);
+
+ mCurrentInputUri = null;
+ Iterator<Uri> it = mPendingInputUris.iterator();
+ while (it.hasNext()) {
+ Uri uri = it.next();
+ if ( ! "file".equals(uri.getScheme())) {
+ continue;
+ }
+ it.remove();
+ mCancelledInputUris.add(uri);
+ mAdapter.setCancelled(uri, true);
}
+
}
+
+ // hand control flow back
+ cryptoOperation();
+
}
@Override
@@ -714,38 +743,83 @@ public class DecryptListFragment
return false;
}
+ private void lookupUnknownKey(final Uri inputUri, long unknownKeyId) {
+
+ final ArrayList<ParcelableKeyRing> keyList;
+ final String keyserver;
+
+ // search config
+ {
+ Preferences prefs = Preferences.getPreferences(getActivity());
+ Preferences.CloudSearchPrefs cloudPrefs =
+ new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver());
+ keyserver = cloudPrefs.keyserver;
+ }
+
+ {
+ ParcelableKeyRing keyEntry = new ParcelableKeyRing(null,
+ KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null);
+ ArrayList<ParcelableKeyRing> selectedEntries = new ArrayList<>();
+ selectedEntries.add(keyEntry);
+
+ keyList = selectedEntries;
+ }
+
+ CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> callback
+ = new CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult>() {
+
+ @Override
+ public ImportKeyringParcel createOperationInput() {
+ return new ImportKeyringParcel(keyList, keyserver);
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(ImportKeyResult result) {
+ retryUri(inputUri);
+ }
+
+ @Override
+ public void onCryptoOperationCancelled() {
+ mAdapter.setProcessingKeyLookup(inputUri, false);
+ }
+
+ @Override
+ public void onCryptoOperationError(ImportKeyResult result) {
+ result.createNotify(getActivity()).show();
+ mAdapter.setProcessingKeyLookup(inputUri, false);
+ }
+
+ @Override
+ public boolean onCryptoSetProgress(String msg, int progress, int max) {
+ return false;
+ }
+ };
+
+ mAdapter.setProcessingKeyLookup(inputUri, true);
+
+ CryptoOperationHelper importOpHelper = new CryptoOperationHelper<>(2, this, callback, null);
+ importOpHelper.cryptoOperation();
+
+ }
+
+
private void deleteFile(Activity activity, Uri uri) {
// we can only ever delete a file once, if we got this far either it's gone or it will never work
mCanDelete = false;
- if ("file".equals(uri.getScheme())) {
- File file = new File(uri.getPath());
- if (file.delete()) {
+ try {
+ int deleted = FileHelper.deleteFileSecurely(activity, uri);
+ if (deleted > 0) {
Notify.create(activity, R.string.file_delete_ok, Style.OK).show();
} else {
Notify.create(activity, R.string.file_delete_none, Style.WARN).show();
}
- return;
- }
-
- if ("content".equals(uri.getScheme())) {
- try {
- int deleted = activity.getContentResolver().delete(uri, null, null);
- if (deleted > 0) {
- Notify.create(activity, R.string.file_delete_ok, Style.OK).show();
- } else {
- Notify.create(activity, R.string.file_delete_none, Style.WARN).show();
- }
- } catch (Exception e) {
- Log.e(Constants.TAG, "exception deleting file", e);
- Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
- }
- return;
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "exception deleting file", e);
+ Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
}
- Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
-
}
public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
@@ -759,6 +833,7 @@ public class DecryptListFragment
int mProgress, mMax;
String mProgressMsg;
OnClickListener mCancelled;
+ boolean mProcessingKeyLookup;
ViewModel(Uri uri) {
mInputUri = uri;
@@ -767,7 +842,7 @@ public class DecryptListFragment
mCancelled = null;
}
- void addResult(InputDataResult result) {
+ void setResult(InputDataResult result) {
mResult = result;
}
@@ -787,6 +862,10 @@ public class DecryptListFragment
mMax = max;
}
+ void setProcessingKeyLookup(boolean processingKeyLookup) {
+ mProcessingKeyLookup = processingKeyLookup;
+ }
+
// Depends on inputUri only
@Override
public boolean equals(Object o) {
@@ -797,8 +876,10 @@ public class DecryptListFragment
return false;
}
ViewModel viewModel = (ViewModel) o;
- return !(mInputUri != null ? !mInputUri.equals(viewModel.mInputUri)
- : viewModel.mInputUri != null);
+ if (mInputUri == null) {
+ return viewModel.mInputUri == null;
+ }
+ return mInputUri.equals(viewModel.mInputUri);
}
// Depends on inputUri only
@@ -853,17 +934,13 @@ public class DecryptListFragment
}
private void bindItemCancelled(ViewHolder holder, ViewModel model) {
- if (holder.vAnimator.getDisplayedChild() != 3) {
- holder.vAnimator.setDisplayedChild(3);
- }
+ holder.vAnimator.setDisplayedChild(3);
holder.vCancelledRetry.setOnClickListener(model.mCancelled);
}
private void bindItemProgress(ViewHolder holder, ViewModel model) {
- if (holder.vAnimator.getDisplayedChild() != 0) {
- holder.vAnimator.setDisplayedChild(0);
- }
+ holder.vAnimator.setDisplayedChild(0);
holder.vProgress.setProgress(model.mProgress);
holder.vProgress.setMax(model.mMax);
@@ -873,11 +950,10 @@ public class DecryptListFragment
}
private void bindItemSuccess(ViewHolder holder, final ViewModel model) {
- if (holder.vAnimator.getDisplayedChild() != 1) {
- holder.vAnimator.setDisplayedChild(1);
- }
+ holder.vAnimator.setDisplayedChild(1);
- KeyFormattingUtils.setStatus(getResources(), holder, model.mResult.mDecryptVerifyResult);
+ KeyFormattingUtils.setStatus(getResources(), holder,
+ model.mResult.mDecryptVerifyResult, model.mProcessingKeyLookup);
int numFiles = model.mResult.getOutputUris().size();
holder.resizeFileList(numFiles, LayoutInflater.from(getActivity()));
@@ -955,6 +1031,13 @@ public class DecryptListFragment
activity.startActivity(intent);
}
});
+ } else {
+ holder.vSignatureLayout.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ lookupUnknownKey(model.mInputUri, keyId);
+ }
+ });
}
}
@@ -983,9 +1066,7 @@ public class DecryptListFragment
}
private void bindItemFailure(ViewHolder holder, final ViewModel model) {
- if (holder.vAnimator.getDisplayedChild() != 2) {
- holder.vAnimator.setDisplayedChild(2);
- }
+ holder.vAnimator.setDisplayedChild(2);
holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId());
@@ -1034,21 +1115,44 @@ public class DecryptListFragment
notifyItemChanged(pos);
}
- public void setCancelled(Uri uri, OnClickListener retryListener) {
+ public void setCancelled(final Uri uri, boolean isCancelled) {
ViewModel newModel = new ViewModel(uri);
int pos = mDataset.indexOf(newModel);
- mDataset.get(pos).setCancelled(retryListener);
+ if (isCancelled) {
+ mDataset.get(pos).setCancelled(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ retryUri(uri);
+ }
+ });
+ } else {
+ mDataset.get(pos).setCancelled(null);
+ }
notifyItemChanged(pos);
}
- public void addResult(Uri uri, InputDataResult result) {
+ public void setProcessingKeyLookup(Uri uri, boolean processingKeyLookup) {
+ ViewModel newModel = new ViewModel(uri);
+ int pos = mDataset.indexOf(newModel);
+ mDataset.get(pos).setProcessingKeyLookup(processingKeyLookup);
+ notifyItemChanged(pos);
+ }
+ public void addResult(Uri uri, InputDataResult result) {
ViewModel model = new ViewModel(uri);
int pos = mDataset.indexOf(model);
model = mDataset.get(pos);
+ model.setResult(result);
+ notifyItemChanged(pos);
+ }
- model.addResult(result);
-
+ public void resetItemData(Uri uri) {
+ ViewModel model = new ViewModel(uri);
+ int pos = mDataset.indexOf(model);
+ model = mDataset.get(pos);
+ model.setResult(null);
+ model.setCancelled(null);
+ model.setProcessingKeyLookup(false);
notifyItemChanged(pos);
}
@@ -1072,7 +1176,7 @@ public class DecryptListFragment
public View vSignatureLayout;
public TextView vSignatureName;
public TextView vSignatureMail;
- public TextView vSignatureAction;
+ public ViewAnimator vSignatureAction;
public View vContextMenu;
public TextView vErrorMsg;
@@ -1115,7 +1219,7 @@ public class DecryptListFragment
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);
+ vSignatureAction = (ViewAnimator) itemView.findViewById(R.id.result_signature_action);
vFileList = (LinearLayout) itemView.findViewById(R.id.file_list);
for (int i = 0; i < vFileList.getChildCount(); i++) {
@@ -1179,7 +1283,7 @@ public class DecryptListFragment
}
@Override
- public TextView getSignatureAction() {
+ public ViewAnimator getSignatureAction() {
return vSignatureAction;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
index 2735eb6b8..db31bd0a1 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -72,6 +72,7 @@ import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log;
@@ -298,7 +299,7 @@ public class KeyListFragment extends LoaderFragment
}
static final String ORDER =
- KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC";
+ KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " COLLATE NOCASE ASC";
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
@@ -787,6 +788,8 @@ public class KeyListFragment extends LoaderFragment
final KeyItemViewHolder holder = (KeyItemViewHolder) view.getTag();
holder.mSlinger.setVisibility(View.VISIBLE);
+
+ ContentDescriptionHint.setup(holder.mSlingerButton);
holder.mSlingerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
index 7b761a52f..c333ee0ef 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -82,6 +82,7 @@ import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
@@ -182,6 +183,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements
mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout);
mRotateSpin = AnimationUtils.loadAnimation(this, R.anim.rotate_spin);
+
+ //ContentDescriptionHint Listeners implemented
+
+ ContentDescriptionHint.setup(mActionEncryptFile);
+ ContentDescriptionHint.setup(mActionEncryptText);
+ ContentDescriptionHint.setup(mActionNfc);
+ ContentDescriptionHint.setup(mFab);
+
+
mRotateSpin.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java
new file mode 100644
index 000000000..8e45a20e9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ContentDescriptionHint.java
@@ -0,0 +1,101 @@
+package org.sufficientlysecure.keychain.ui.util;
+
+/**
+ * Created by rohan on 20/9/15.
+ */
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.Toast;
+public class ContentDescriptionHint {
+ private static final int ESTIMATED_TOAST_HEIGHT_DIPS = 48;
+ public static void setup(View view) {
+ view.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ return showLongClickText(view, view.getContentDescription());
+ }
+ });
+ }
+
+ public static void setup(View view, final int textResId) {
+ view.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ return showLongClickText(view, view.getContext().getString(textResId));
+ }
+ });
+ }
+
+ public static void setup(View view, final CharSequence text) {
+ view.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ return showLongClickText(view, text);
+ }
+ });
+ }
+
+ public static void remove(final View view) {
+ view.setOnLongClickListener(null);
+ }
+
+ private static boolean showLongClickText(View view, CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ return false;
+ }
+
+ final int[] screenPos = new int[2]; // origin is device display
+ final Rect displayFrame = new Rect(); // includes decorations (e.g. status bar)
+ view.getLocationOnScreen(screenPos);
+ view.getWindowVisibleDisplayFrame(displayFrame);
+
+ final Context context = view.getContext();
+ final int viewWidth = view.getWidth();
+ final int viewHeight = view.getHeight();
+ final int viewCenterX = screenPos[0] + viewWidth / 2;
+ final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ final int estimatedToastHeight = (int) (ESTIMATED_TOAST_HEIGHT_DIPS
+ * context.getResources().getDisplayMetrics().density);
+
+ Toast longClickText = Toast.makeText(context, text, Toast.LENGTH_SHORT);
+ boolean showBelow = screenPos[1] < estimatedToastHeight;
+ if (showBelow) {
+ // Show below
+ // Offsets are after decorations (e.g. status bar) are factored in
+ longClickText.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ viewCenterX - screenWidth / 2,
+ screenPos[1] - displayFrame.top + viewHeight);
+ } else {
+ // Show above
+ // Offsets are after decorations (e.g. status bar) are factored in
+ // NOTE: We can't use Gravity.BOTTOM because when the keyboard is up
+ // its height isn't factored in.
+ longClickText.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ viewCenterX - screenWidth / 2,
+ screenPos[1] - displayFrame.top - estimatedToastHeight);
+ }
+
+ longClickText.show();
+ return true;
+ }
+
+}
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 9ab0db03e..b9b837d71 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
@@ -28,6 +28,7 @@ import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
+import android.widget.ViewAnimator;
import org.openintents.openpgp.OpenPgpDecryptionResult;
import org.openintents.openpgp.OpenPgpSignatureResult;
@@ -440,14 +441,15 @@ public class KeyFormattingUtils {
View getSignatureLayout();
TextView getSignatureUserName();
TextView getSignatureUserEmail();
- TextView getSignatureAction();
+ ViewAnimator getSignatureAction();
boolean hasEncrypt();
}
@SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated
- public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result) {
+ public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result,
+ boolean processingkeyLookup) {
if (holder.hasEncrypt()) {
OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult();
@@ -488,7 +490,7 @@ public class KeyFormattingUtils {
OpenPgpSignatureResult signatureResult = result.getSignatureResult();
int sigText, sigIcon, sigColor;
- int sigActionText, sigActionIcon;
+ int sigActionDisplayedChild;
switch (signatureResult.getResult()) {
@@ -500,8 +502,7 @@ public class KeyFormattingUtils {
sigColor = R.color.key_flag_gray;
// won't be used, but makes compiler happy
- sigActionText = 0;
- sigActionIcon = 0;
+ sigActionDisplayedChild = -1;
break;
}
@@ -510,8 +511,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_verified_cutout_24dp;
sigColor = R.color.key_flag_green;
- sigActionText = R.string.decrypt_result_action_show;
- sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ sigActionDisplayedChild = 0;
break;
}
@@ -520,8 +520,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_unverified_cutout_24dp;
sigColor = R.color.key_flag_orange;
- sigActionText = R.string.decrypt_result_action_show;
- sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ sigActionDisplayedChild = 0;
break;
}
@@ -530,8 +529,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_revoked_cutout_24dp;
sigColor = R.color.key_flag_red;
- sigActionText = R.string.decrypt_result_action_show;
- sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ sigActionDisplayedChild = 0;
break;
}
@@ -540,8 +538,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_expired_cutout_24dp;
sigColor = R.color.key_flag_red;
- sigActionText = R.string.decrypt_result_action_show;
- sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ sigActionDisplayedChild = 0;
break;
}
@@ -550,8 +547,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_unknown_cutout_24dp;
sigColor = R.color.key_flag_red;
- sigActionText = R.string.decrypt_result_action_Lookup;
- sigActionIcon = R.drawable.ic_file_download_grey_24dp;
+ sigActionDisplayedChild = 1;
break;
}
@@ -560,8 +556,7 @@ public class KeyFormattingUtils {
sigIcon = R.drawable.status_signature_invalid_cutout_24dp;
sigColor = R.color.key_flag_red;
- sigActionText = R.string.decrypt_result_action_show;
- sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
+ sigActionDisplayedChild = 0;
break;
}
@@ -572,13 +567,17 @@ public class KeyFormattingUtils {
sigColor = R.color.key_flag_red;
// won't be used, but makes compiler happy
- sigActionText = 0;
- sigActionIcon = 0;
+ sigActionDisplayedChild = -1;
break;
}
}
+ // possibly switch out "Lookup" button for progress bar
+ if (sigActionDisplayedChild == 1 && processingkeyLookup) {
+ sigActionDisplayedChild = 2;
+ }
+
int sigColorRes = resources.getColor(sigColor);
holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN);
holder.getSignatureStatusIcon().setImageDrawable(resources.getDrawable(sigIcon));
@@ -591,9 +590,7 @@ public class KeyFormattingUtils {
holder.getSignatureLayout().setVisibility(View.VISIBLE);
- holder.getSignatureAction().setText(sigActionText);
- holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds(
- 0, 0, sigActionIcon, 0);
+ holder.getSignatureAction().setDisplayedChild(sigActionDisplayedChild);
String userId = result.getSignatureResult().getPrimaryUserId();
KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
index 7345faad9..bae119700 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
@@ -25,7 +25,9 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
import java.text.DecimalFormat;
import android.annotation.TargetApi;
@@ -33,7 +35,6 @@ import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
@@ -41,20 +42,13 @@ import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Environment;
-import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.support.v4.app.Fragment;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.StructStat;
import android.widget.Toast;
-import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import static android.system.OsConstants.S_IROTH;
-
/** This class offers a number of helper functions for saving documents.
*
* There are three entry points here: openDocument, saveDocument and
@@ -167,6 +161,14 @@ public class FileHelper {
}
public static long getFileSize(Context context, Uri uri, long def) {
+ if ("file".equals(uri.getScheme())) {
+ long size = new File(uri.getPath()).length();
+ if (size == 0) {
+ size = def;
+ }
+ return size;
+ }
+
long size = def;
try {
Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null);
@@ -261,6 +263,44 @@ public class FileHelper {
}
}
+ /**
+ * Deletes data at a URI securely by overwriting it with random data
+ * before deleting it. This method is fail-fast - if we can't securely
+ * delete the file, we don't delete it at all.
+ */
+ public static int deleteFileSecurely(Context context, Uri uri)
+ throws IOException {
+
+ ContentResolver resolver = context.getContentResolver();
+ long lengthLeft = FileHelper.getFileSize(context, uri);
+
+ if (lengthLeft == -1) {
+ throw new IOException("Error opening file!");
+ }
+
+ SecureRandom random = new SecureRandom();
+ byte[] randomData = new byte[1024];
+
+ OutputStream out = resolver.openOutputStream(uri, "w");
+ if (out == null) {
+ throw new IOException("Error opening file!");
+ }
+ out = new BufferedOutputStream(out);
+ while (lengthLeft > 0) {
+ random.nextBytes(randomData);
+ out.write(randomData, 0, lengthLeft > randomData.length ? randomData.length : (int) lengthLeft);
+ lengthLeft -= randomData.length;
+ }
+ out.close();
+
+ if ("file".equals(uri.getScheme())) {
+ return new File(uri.getPath()).delete() ? 1 : 0;
+ } else {
+ return resolver.delete(uri, null, null);
+ }
+
+ }
+
/** Checks if external storage is mounted if file is located on external storage. */
public static boolean isStorageMounted(String file) {
if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
diff --git a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
index 4a79f0ccd..8bdfce733 100644
--- a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
+++ b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
@@ -26,7 +26,6 @@
android:measureAllChildren="false"
custom:initialView="1"
android:minHeight="?listPreferredItemHeightSmall"
- android:animateLayoutChanges="true"
>
<LinearLayout
@@ -113,6 +112,7 @@
android:id="@+id/result_signature_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
android:padding="4dp"
android:src="@drawable/status_signature_unverified_cutout_24dp"
/>
@@ -136,6 +136,7 @@
android:clickable="true"
android:background="?android:selectableItemBackground"
android:orientation="horizontal"
+ android:minHeight="40dp"
>
<LinearLayout
@@ -144,7 +145,7 @@
android:layout_weight="1"
android:paddingRight="4dp"
android:paddingLeft="4dp"
- android:gravity="center_vertical"
+ android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
@@ -162,6 +163,8 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text=""
+ android:singleLine="true"
+ android:ellipsize="end"
tools:text="alice@example.com" />
</LinearLayout>
@@ -174,19 +177,45 @@
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"
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
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"
- />
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:id="@+id/result_signature_action"
+ android:measureAllChildren="true"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="0"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:drawableRight="@drawable/ic_vpn_key_grey_24dp"
+ android:drawablePadding="8dp"
+ android:text="@string/decrypt_result_action_show"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:drawableRight="@drawable/ic_file_download_grey_24dp"
+ android:drawablePadding="8dp"
+ android:text="@string/decrypt_result_action_Lookup"
+ />
+
+ <ProgressBar
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:padding="4dp"
+ android:layout_gravity="center" />
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout/key_list_item.xml b/OpenKeychain/src/main/res/layout/key_list_item.xml
index 80be0ec34..d9e7170c5 100644
--- a/OpenKeychain/src/main/res/layout/key_list_item.xml
+++ b/OpenKeychain/src/main/res/layout/key_list_item.xml
@@ -123,7 +123,8 @@
android:layout_gravity="center"
android:src="@drawable/ic_repeat_grey_24dp"
android:padding="12dp"
- android:background="?android:selectableItemBackground" />
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/cd_exchange_keys"/>
</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout/view_key_activity.xml b/OpenKeychain/src/main/res/layout/view_key_activity.xml
index 560180407..7c021bec2 100644
--- a/OpenKeychain/src/main/res/layout/view_key_activity.xml
+++ b/OpenKeychain/src/main/res/layout/view_key_activity.xml
@@ -94,6 +94,7 @@
<ImageButton
android:id="@+id/view_key_action_encrypt_files"
+ android:contentDescription="@string/cd_encrypt_files"
style="?android:attr/borderlessButtonStyle"
android:layout_width="64dp"
android:layout_height="64dp"
@@ -103,6 +104,7 @@
<ImageButton
android:id="@+id/view_key_action_encrypt_text"
+ android:contentDescription="@string/cd_encrypt_text"
style="?android:attr/borderlessButtonStyle"
android:layout_width="64dp"
android:layout_height="64dp"
@@ -113,6 +115,7 @@
<ImageButton
android:id="@+id/view_key_action_nfc"
style="?android:attr/borderlessButtonStyle"
+ android:contentDescription="@string/cd_share_nfc"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_nfc_white_24dp"
@@ -213,6 +216,7 @@
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
+ android:contentDescription="@string/cd_exchange_keys"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index 8dc6573a6..ff148b0c7 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -106,6 +106,12 @@
<string name="btn_saved">"Saved!"</string>
<string name="btn_not_matching">"Doesn't match"</string>
+ <!-- Content Description -->
+ <string name="cd_encrypt_files">"Encrypt Files"</string>
+ <string name="cd_exchange_keys">"Exchange Keys"</string>
+ <string name="cd_encrypt_text">"Encrypt Text"</string>
+ <string name="cd_share_nfc">"Share Via NFC"</string>
+
<!-- menu -->
<string name="menu_preferences">"Settings"</string>
<string name="menu_help">"Help"</string>
@@ -1385,7 +1391,6 @@
<string name="msg_data_detached_trailing">"Skipping trailing data after signed part!"</string>
<string name="msg_data_detached_unsupported">"Unsupported type of detached signature!"</string>
<string name="msg_data_error_io">"Error reading input data!"</string>
- <string name="msg_data_error_openpgp">"Error processing OpenPGP data!"</string>
<string name="msg_data_mime_bad">"Could not parse as MIME data"</string>
<string name="msg_data_mime_filename">"Filename: '%s'"</string>
<string name="msg_data_mime_from_extension">"Guessing MIME type from extension"</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 d3c3f1df5..47c7a20ea 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
@@ -556,7 +556,7 @@ public class PgpEncryptDecryptTest {
}
@Test
- public void testAsymmetricMultiSubkeyEncrypt() throws Exception {
+ public void testMultiSubkeyEncryptSkipStripOrBadFlag() throws Exception {
String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
@@ -610,7 +610,8 @@ public class PgpEncryptDecryptTest {
{ // strip first encrypted subkey, decryption should skip it
- SaveKeyringParcel parcel = new SaveKeyringParcel(mStaticRing1.getMasterKeyId(), mStaticRing1.getFingerprint());
+ SaveKeyringParcel parcel =
+ new SaveKeyringParcel(mStaticRing1.getMasterKeyId(), mStaticRing1.getFingerprint());
parcel.mChangeSubKeys.add(new SubkeyChange(encKeyId1, true, false));
UncachedKeyRing modified = PgpKeyOperationTest.applyModificationWithChecks(parcel, mStaticRing1,
new ArrayList<RawPacket>(), new ArrayList<RawPacket>(),
@@ -631,8 +632,9 @@ public class PgpEncryptDecryptTest {
{ // change flags of second encrypted subkey, decryption should skip it
- SaveKeyringParcel parcel = new SaveKeyringParcel(mStaticRing1.getMasterKeyId(), mStaticRing1.getFingerprint());
- parcel.mChangeSubKeys.add(new SubkeyChange(encKeyId1, PGPKeyFlags.CAN_CERTIFY, null));
+ SaveKeyringParcel parcel =
+ new SaveKeyringParcel(mStaticRing1.getMasterKeyId(), mStaticRing1.getFingerprint());
+ parcel.mChangeSubKeys.add(new SubkeyChange(encKeyId1, KeyFlags.CERTIFY_OTHER, null));
UncachedKeyRing modified = PgpKeyOperationTest.applyModificationWithChecks(parcel, mStaticRing1,
new ArrayList<RawPacket>(), new ArrayList<RawPacket>(),
new CryptoInputParcel(new Date(), mKeyPhrase1));
@@ -650,6 +652,13 @@ public class PgpEncryptDecryptTest {
result.getLog().containsType(LogType.MSG_DC_ASKIP_BAD_FLAGS));
}
+ }
+
+ @Test
+ public void testMultiSubkeyEncryptSkipRevoked() throws Exception {
+
+ String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
+
{ // revoke first encryption subkey of keyring in database
SaveKeyringParcel parcel = new SaveKeyringParcel(mStaticRing1.getMasterKeyId(), mStaticRing1.getFingerprint());
parcel.mRevokeSubKeys.add(KeyringTestingHelper.getSubkeyId(mStaticRing1, 2));
@@ -679,7 +688,7 @@ public class PgpEncryptDecryptTest {
data, out);
Assert.assertTrue("encryption must succeed", result.success());
- ciphertext = out.toByteArray();
+ byte[] ciphertext = out.toByteArray();
Iterator<RawPacket> packets = KeyringTestingHelper.parseKeyring(ciphertext);
diff --git a/README.md b/README.md
index 3144b4b77..f3e839611 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[![Stories in Ready](https://badge.waffle.io/open-keychain/open-keychain.png?label=ready&title=Ready)](https://waffle.io/open-keychain/open-keychain)
# OpenKeychain (for Android)
OpenKeychain is an OpenPGP implementation for Android.