aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Graphics/drawables/status_signature_verified_inner.svg77
-rwxr-xr-xGraphics/update-drawables.sh6
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java8
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/KeyFormattingUtilsTest.java44
-rw-r--r--OpenKeychain/build.gradle1
-rw-r--r--OpenKeychain/src/main/AndroidManifest.xml4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java49
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java176
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java88
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java122
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java41
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java127
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java142
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GithubResource.java218
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java242
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java15
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java48
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java15
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java350
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java205
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java227
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java57
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java19
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java132
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java154
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java262
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep1Fragment.java129
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep2Fragment.java111
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java128
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java172
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java127
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java112
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java103
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java594
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java106
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java145
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java38
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java76
-rw-r--r--OpenKeychain/src/main/res/anim/fade_in.xml7
-rw-r--r--OpenKeychain/src/main/res/anim/fade_in_up.xml11
-rw-r--r--OpenKeychain/src/main/res/anim/fade_out.xml7
-rw-r--r--OpenKeychain/src/main/res/anim/fade_out_down.xml11
-rw-r--r--OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_24dp.pngbin0 -> 544 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_96dp.pngbin0 -> 2031 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_24dp.pngbin0 -> 429 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_96dp.pngbin0 -> 1437 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_24dp.pngbin0 -> 717 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_96dp.pngbin0 -> 3022 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_24dp.pngbin0 -> 1095 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_96dp.pngbin0 -> 4214 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xxxhdpi/status_signature_verified_inner_24dp.pngbin0 -> 1437 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable/divider.xml11
-rw-r--r--OpenKeychain/src/main/res/drawable/dns.pngbin0 -> 2043 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable/github.pngbin0 -> 1714 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable/ssl_lock.pngbin0 -> 479 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable/twitter.pngbin0 -> 5122 bytes
-rw-r--r--OpenKeychain/src/main/res/layout/cert_list_widget.xml44
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step1.xml140
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step2.xml164
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_github_fragment_step1.xml134
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_github_fragment_step2.xml152
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_https_fragment_step1.xml142
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_https_fragment_step2.xml165
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step1.xml134
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step2.xml152
-rw-r--r--OpenKeychain/src/main/res/layout/linked_create_verify.xml79
-rw-r--r--OpenKeychain/src/main/res/layout/linked_id_item.xml72
-rw-r--r--OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml180
-rw-r--r--OpenKeychain/src/main/res/layout/linked_select_fragment.xml188
-rw-r--r--OpenKeychain/src/main/res/layout/view_key_fragment.xml54
-rw-r--r--OpenKeychain/src/main/res/menu/key_view.xml5
-rw-r--r--OpenKeychain/src/main/res/transition/linked_id_card_trans.xml4
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml82
-rw-r--r--README.md9
86 files changed, 6384 insertions, 300 deletions
diff --git a/Graphics/drawables/status_signature_verified_inner.svg b/Graphics/drawables/status_signature_verified_inner.svg
new file mode 100644
index 000000000..554df0643
--- /dev/null
+++ b/Graphics/drawables/status_signature_verified_inner.svg
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100px"
+ height="100px"
+ viewBox="0 0 100 100"
+ version="1.1"
+ id="svg2"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="status_signature_verified_cutout.svg">
+ <metadata
+ id="metadata16">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>signature-verified-cutout</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="784"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="2.36"
+ inkscape:cx="50"
+ inkscape:cy="50"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="signature-verified-cutout" />
+ <!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
+ <title
+ id="title4">signature-verified-cutout</title>
+ <desc
+ id="desc6">Created with Sketch.</desc>
+ <defs
+ id="defs8" />
+ <g
+ id="Page-1"
+ sketch:type="MSPage"
+ stroke-width="1"
+ stroke="none"
+ fill-rule="evenodd"
+ fill="none">
+ <g
+ id="signature-verified-cutout"
+ sketch:type="MSArtboardGroup"
+ transform="translate(0.110156, 0.000000)"
+ fill="#000000">
+ <path
+ d="M 46.273291,77.5085 20,57.830916 27.91844,47.63497 43.309686,59.515226 70.31112,23 80.867825,30.778219 z"
+ sketch:type="MSShapeGroup"
+ id="path12"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/Graphics/update-drawables.sh b/Graphics/update-drawables.sh
index 8da894725..a2da5bbfc 100755
--- a/Graphics/update-drawables.sh
+++ b/Graphics/update-drawables.sh
@@ -22,7 +22,7 @@ SRC_DIR=./drawables/
#inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg
-for NAME in "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign"
+for NAME in "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "status_signature_verified_inner"
do
echo $NAME
inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
@@ -32,7 +32,7 @@ inkscape -w 72 -h 72 -e "$XXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
inkscape -w 96 -h 96 -e "$XXXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
done
-for NAME in "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout"
+for NAME in "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "status_signature_verified_inner"
do
echo $NAME
inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
@@ -60,4 +60,4 @@ for NAME in "first_time_1"
do
echo $NAME
inkscape -w 512 -h 512 -e "$DRAWABLE_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
-done \ No newline at end of file
+done
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
index 7c4b2e91e..92482d004 100644
--- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
@@ -164,7 +164,7 @@ public class CertifyOperationTest {
CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
actions.add(new CertifyAction(mStaticRing2.getMasterKeyId(),
- mStaticRing2.getPublicKey().getUnorderedUserIds()));
+ mStaticRing2.getPublicKey().getUnorderedUserIds(), null));
CertifyResult result = op.certify(actions, null);
Assert.assertTrue("certification must succeed", result.success());
@@ -214,7 +214,7 @@ public class CertifyOperationTest {
CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
actions.add(new CertifyAction(mStaticRing1.getMasterKeyId(),
- mStaticRing2.getPublicKey().getUnorderedUserIds()));
+ mStaticRing2.getPublicKey().getUnorderedUserIds(), null));
CertifyResult result = op.certify(actions, null);
@@ -232,7 +232,7 @@ public class CertifyOperationTest {
CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
ArrayList<String> uids = new ArrayList<String>();
uids.add("nonexistent");
- actions.add(new CertifyAction(1234L, uids));
+ actions.add(new CertifyAction(1234L, uids, null));
CertifyResult result = op.certify(actions, null);
@@ -244,7 +244,7 @@ public class CertifyOperationTest {
{
CertifyActionsParcel actions = new CertifyActionsParcel(1234L);
actions.add(new CertifyAction(mStaticRing1.getMasterKeyId(),
- mStaticRing2.getPublicKey().getUnorderedUserIds()));
+ mStaticRing2.getPublicKey().getUnorderedUserIds(), null));
CertifyResult result = op.certify(actions, null);
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/KeyFormattingUtilsTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/KeyFormattingUtilsTest.java
new file mode 100644
index 000000000..6aa4e7b8e
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/KeyFormattingUtilsTest.java
@@ -0,0 +1,44 @@
+package org.sufficientlysecure.keychain.util;
+
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class KeyFormattingUtilsTest {
+
+ static final byte[] fp = new byte[] {
+ (byte) 0xD4, (byte) 0xAB, (byte) 0x19, (byte) 0x29, (byte) 0x64,
+ (byte) 0xF7, (byte) 0x6A, (byte) 0x7F, (byte) 0x8F, (byte) 0x8A,
+ (byte) 0x9B, (byte) 0x35, (byte) 0x7B, (byte) 0xD1, (byte) 0x83,
+ (byte) 0x20, (byte) 0xDE, (byte) 0xAD, (byte) 0xFA, (byte) 0x11
+ };
+ static final long keyId = 0x7bd18320deadfa11L;
+
+ @Test
+ public void testStuff() {
+ Assert.assertEquals(KeyFormattingUtils.convertFingerprintToKeyId(fp), keyId);
+
+ Assert.assertEquals(
+ "d4ab192964f76a7f8f8a9b357bd18320deadfa11",
+ KeyFormattingUtils.convertFingerprintToHex(fp)
+ );
+
+ Assert.assertEquals(
+ "0x7bd18320deadfa11",
+ KeyFormattingUtils.convertKeyIdToHex(keyId)
+ );
+
+ Assert.assertEquals(
+ "0xdeadfa11",
+ KeyFormattingUtils.convertKeyIdToHexShort(keyId)
+ );
+
+ }
+
+}
diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle
index b3ab0b3ef..00f291468 100644
--- a/OpenKeychain/build.gradle
+++ b/OpenKeychain/build.gradle
@@ -19,6 +19,7 @@ dependencies {
compile 'com.jpardogo.materialtabstrip:library:1.0.9'
compile 'it.neokree:MaterialNavigationDrawer:1.3.1'
compile 'com.getbase:floatingactionbutton:1.8.0'
+ compile 'com.ocpsoft:ocpsoft-pretty-time:1.0.6'
// libs as submodules
compile project(':extern:openpgp-api-lib')
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index d9f9766c9..6cfc0f5a1 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -101,6 +101,10 @@
android:label="@string/title_edit_key" />
<!-- NOTE: Dont use configChanges for QR Code view! We use a different layout for landscape -->
<activity
+ android:name=".ui.linked.LinkedIdWizard"
+ android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
+ android:label="@string/title_linked_create" />
+ <activity
android:name=".ui.QrCodeViewActivity"
android:label="@string/share_qr_code_dialog_title" />
<activity
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
new file mode 100644
index 000000000..42e9ec3f0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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.operations.results;
+
+import android.os.Parcel;
+
+public class LinkedVerifyResult extends OperationResult {
+
+ public LinkedVerifyResult(int result, OperationLog log) {
+ super(result, log);
+ }
+
+ /** Construct from a parcel - trivial because we have no extra data. */
+ public LinkedVerifyResult(Parcel source) {
+ super(source);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ }
+
+ public static Creator<LinkedVerifyResult> CREATOR = new Creator<LinkedVerifyResult>() {
+ public LinkedVerifyResult createFromParcel(final Parcel source) {
+ return new LinkedVerifyResult(source);
+ }
+
+ public LinkedVerifyResult[] newArray(final int size) {
+ return new LinkedVerifyResult[size];
+ }
+ };
+
+}
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 f2a27b0fc..616b6f062 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
@@ -765,6 +765,20 @@ public abstract class OperationResult implements Parcelable {
MSG_DEL_OK (LogLevel.OK, R.plurals.msg_del_ok),
MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail),
+ MSG_LV (LogLevel.START, R.string.msg_lv),
+ MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
+ MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error),
+ MSG_LV_FP_OK (LogLevel.DEBUG, R.string.msg_lv_fp_ok),
+ MSG_LV_FP_ERROR (LogLevel.ERROR, R.string.msg_lv_fp_error),
+
+ MSG_LV_FETCH (LogLevel.DEBUG, R.string.msg_lv_fetch),
+ MSG_LV_FETCH_REDIR (LogLevel.DEBUG, R.string.msg_lv_fetch_redir),
+ MSG_LV_FETCH_OK (LogLevel.DEBUG, R.string.msg_lv_fetch_ok),
+ MSG_LV_FETCH_ERROR (LogLevel.ERROR, R.string.msg_lv_fetch_error),
+ MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url),
+ MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io),
+ MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format),
+
//export log
MSG_EXPORT_LOG(LogLevel.START,R.string.msg_export_log_start),
MSG_EXPORT_LOG_EXPORT_ERROR_NO_FILE(LogLevel.ERROR,R.string.msg_export_log_error_no_file),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
index 9276cba10..0173a1d83 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
@@ -361,4 +361,5 @@ public class UncachedPublicKey {
return calendar.getTime();
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
index 2c7f0187a..2431cb743 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
@@ -1,4 +1,8 @@
/*
+<<<<<<< HEAD
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+=======
+>>>>>>> development
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -37,6 +41,7 @@ public class WrappedUserAttribute implements Serializable {
public static final int UAT_NONE = 0;
public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE;
+ public static final int UAT_LINKED_ID = 101;
private PGPUserAttributeSubpacketVector mVector;
@@ -77,7 +82,7 @@ public class WrappedUserAttribute implements Serializable {
public static WrappedUserAttribute fromData (byte[] data) throws IOException {
UserAttributeSubpacketInputStream in =
new UserAttributeSubpacketInputStream(new ByteArrayInputStream(data));
- ArrayList<UserAttributeSubpacket> list = new ArrayList<UserAttributeSubpacket>();
+ ArrayList<UserAttributeSubpacket> list = new ArrayList<>();
while (in.available() > 0) {
list.add(in.readPacket());
}
@@ -121,6 +126,7 @@ public class WrappedUserAttribute implements Serializable {
private void readObjectNoData() throws ObjectStreamException {
}
+ @SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (!WrappedUserAttribute.class.isInstance(o)) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java
new file mode 100644
index 000000000..940e0f7eb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java
@@ -0,0 +1,176 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import android.content.Context;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+public abstract class LinkedCookieResource extends LinkedResource {
+
+ protected LinkedCookieResource(Set<String> flags, HashMap<String, String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public URI toUri () {
+
+ StringBuilder b = new StringBuilder();
+ b.append("pgpid+cookie:");
+
+ // add flags
+ if (mFlags != null) {
+ boolean first = true;
+ for (String flag : mFlags) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(flag);
+ }
+ }
+
+ // add parameters
+ if (mParams != null) {
+ boolean first = true;
+ for (Entry<String, String> stringStringEntry : mParams.entrySet()) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue());
+ }
+ }
+
+ b.append("@");
+ b.append(mSubUri);
+
+ return URI.create(b.toString());
+
+ }
+
+ public URI getSubUri () {
+ return mSubUri;
+ }
+
+ public static String generate (Context context, byte[] fingerprint) {
+ return String.format("[Verifying my PGP key: openpgp4fpr:%s]",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint));
+ }
+
+ public static String generatePreview () {
+ return "[Verifying my PGP key: openpgp4fpr:0x…]";
+ }
+
+ public LinkedVerifyResult verify(byte[] fingerprint) {
+
+ OperationLog log = new OperationLog();
+ log.add(LogType.MSG_LV, 0);
+
+ // Try to fetch resource. Logs for itself
+ String res = fetchResource(log, 1);
+ if (res == null) {
+ // if this is null, an error was recorded in fetchResource above
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ Log.d(Constants.TAG, "Resource data: '" + res + "'");
+
+ return verifyString(log, 1, res, fingerprint);
+
+ }
+
+ protected abstract String fetchResource (OperationLog log, int indent);
+
+ protected Matcher matchResource (OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+
+ protected LinkedVerifyResult verifyString (OperationLog log, int indent,
+ String res,
+ byte[] fingerprint) {
+
+ log.add(LogType.MSG_LV_MATCH, indent);
+ Matcher match = matchResource(log, indent+1, res);
+ if (!match.find()) {
+ log.add(LogType.MSG_LV_MATCH_ERROR, 2);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ String candidateFp = match.group(1).toLowerCase();
+ String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint);
+ if (!fp.equals(candidateFp)) {
+ log.add(LogType.MSG_LV_FP_ERROR, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+ log.add(LogType.MSG_LV_FP_OK, indent);
+
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log);
+
+ }
+
+ public static String getResponseBody(HttpRequestBase request) throws IOException, HttpStatusException {
+ StringBuilder sb = new StringBuilder();
+
+ DefaultHttpClient httpClient = new DefaultHttpClient(new BasicHttpParams());
+ HttpResponse response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String reason = response.getStatusLine().getReasonPhrase();
+
+ if (statusCode != 200) {
+ throw new HttpStatusException(statusCode, reason);
+ }
+
+ HttpEntity entity = response.getEntity();
+ InputStream inputStream = entity.getContent();
+
+ BufferedReader bReader = new BufferedReader(
+ new InputStreamReader(inputStream, "UTF-8"), 8);
+ String line;
+ while ((line = bReader.readLine()) != null) {
+ sb.append(line);
+ }
+
+ return sb.toString();
+ }
+
+ public static class HttpStatusException extends Throwable {
+
+ private final int mStatusCode;
+ private final String mReason;
+
+ HttpStatusException(int statusCode, String reason) {
+ super("http status " + statusCode + ": " + reason);
+ mStatusCode = statusCode;
+ mReason = reason;
+ }
+
+ public int getStatus() {
+ return mStatusCode;
+ }
+
+ public String getReason() {
+ return mReason;
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java
new file mode 100644
index 000000000..ed3031b84
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java
@@ -0,0 +1,88 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.spongycastle.bcpg.UserAttributeSubpacket;
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+
+
+public class LinkedIdentity extends RawLinkedIdentity {
+
+ public final LinkedResource mResource;
+
+ protected LinkedIdentity(URI uri, LinkedResource resource) {
+ super(uri);
+ if (resource == null) {
+ throw new AssertionError("resource must not be null in a LinkedIdentity!");
+ }
+ mResource = resource;
+ }
+
+ public static RawLinkedIdentity fromAttributeData(byte[] data) throws IOException {
+ WrappedUserAttribute att = WrappedUserAttribute.fromData(data);
+
+ byte[][] subpackets = att.getSubpackets();
+ if (subpackets.length >= 1) {
+ return fromSubpacketData(subpackets[0]);
+ }
+
+ throw new IOException("no subpacket data");
+ }
+
+ /** This method parses a linked id from a UserAttributeSubpacket, or returns null if the
+ * subpacket can not be parsed as a valid linked id.
+ */
+ static RawLinkedIdentity fromAttributeSubpacket(UserAttributeSubpacket subpacket) {
+ if (subpacket.getType() != 101) {
+ return null;
+ }
+
+ byte[] data = subpacket.getData();
+
+ return fromSubpacketData(data);
+ }
+
+ static RawLinkedIdentity fromSubpacketData(byte[] data) {
+
+ try {
+ String uriStr = Strings.fromUTF8ByteArray(data);
+ URI uri = URI.create(uriStr);
+
+ LinkedResource res = LinkedResource.fromUri(uri);
+ if (res == null) {
+ return new RawLinkedIdentity(uri);
+ }
+
+ return new LinkedIdentity(uri, res);
+
+ } catch (IllegalArgumentException e) {
+ Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
+ return null;
+ }
+ }
+
+ public static RawLinkedIdentity fromResource (LinkedCookieResource res) {
+ return new RawLinkedIdentity(res.toUri());
+ }
+
+
+ public @DrawableRes int getDisplayIcon() {
+ return mResource.getDisplayIcon();
+ }
+
+ public String getDisplayTitle(Context context) {
+ return mResource.getDisplayTitle(context);
+ }
+
+ public String getDisplayComment(Context context) {
+ return mResource.getDisplayComment(context);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java
new file mode 100644
index 000000000..476d0b21e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java
@@ -0,0 +1,122 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GithubResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.TwitterResource;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+
+public abstract class LinkedResource {
+
+ protected final URI mSubUri;
+ protected final Set<String> mFlags;
+ protected final HashMap<String,String> mParams;
+
+ public static Pattern magicPattern =
+ Pattern.compile("\\[Verifying my PGP key: openpgp4fpr:([a-zA-Z0-9]+)]");
+
+ protected LinkedResource(Set<String> flags, HashMap<String, String> params, URI uri) {
+ mFlags = flags;
+ mParams = params;
+ mSubUri = uri;
+ }
+
+ public Set<String> getFlags () {
+ return new HashSet<>(mFlags);
+ }
+
+ public HashMap<String,String> getParams () {
+ return new HashMap<>(mParams);
+ }
+
+ protected static LinkedCookieResource fromUri (URI uri) {
+
+ if (!"pgpid+cookie".equals(uri.getScheme())) {
+ Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet");
+ return null;
+ }
+
+ if (!uri.isOpaque()) {
+ Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet");
+ return null;
+ }
+
+ String specific = uri.getSchemeSpecificPart();
+ if (!specific.contains("@")) {
+ Log.e(Constants.TAG, "unknown uri scheme in linked id packet");
+ return null;
+ }
+
+ String[] pieces = specific.split("@", 2);
+ URI subUri = URI.create(pieces[1]);
+
+ Set<String> flags = new HashSet<>();
+ HashMap<String,String> params = new HashMap<>();
+ if (!pieces[0].isEmpty()) {
+ String[] rawParams = pieces[0].split(";");
+ for (String param : rawParams) {
+ String[] p = param.split("=", 2);
+ if (p.length == 1) {
+ flags.add(param);
+ } else {
+ params.put(p[0], p[1]);
+ }
+ }
+ }
+
+ return findResourceType(flags, params, subUri);
+
+ }
+
+ protected static LinkedCookieResource findResourceType (Set<String> flags,
+ HashMap<String,String> params,
+ URI subUri) {
+
+ LinkedCookieResource res;
+
+ res = GenericHttpsResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ res = DnsResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ res = TwitterResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ res = GithubResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+
+ return null;
+
+ }
+
+ public abstract @DrawableRes int getDisplayIcon();
+ public abstract @StringRes int getVerifiedText();
+ public abstract String getDisplayTitle(Context context);
+ public abstract String getDisplayComment(Context context);
+ public boolean isViewable() {
+ return false;
+ }
+ public Intent getViewIntent() {
+ return null;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java
new file mode 100644
index 000000000..b3acc6790
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java
@@ -0,0 +1,41 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+
+import java.net.URI;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+
+/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */
+public class RawLinkedIdentity {
+
+ public final URI mUri;
+
+ protected RawLinkedIdentity(URI uri) {
+ mUri = uri;
+ }
+
+ public byte[] getEncoded() {
+ return Strings.toUTF8ByteArray(mUri.toASCIIString());
+ }
+
+ public WrappedUserAttribute toUserAttribute () {
+ return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_LINKED_ID, getEncoded());
+ }
+
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.ic_warning_grey_24dp;
+ }
+
+ public String getDisplayTitle(Context context) {
+ return "unknown";
+ }
+
+ public String getDisplayComment(Context context) {
+ return null;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java
new file mode 100644
index 000000000..21c3a3eef
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java
@@ -0,0 +1,127 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Question;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.TXT;
+
+public class DnsResource extends LinkedCookieResource {
+
+ final static Pattern magicPattern =
+ Pattern.compile("pgpid\\+cookie=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)");
+
+ String mFqdn;
+ CLASS mClass;
+ TYPE mType;
+
+ DnsResource(Set<String> flags, HashMap<String, String> params, URI uri,
+ String fqdn, CLASS clazz, TYPE type) {
+ super(flags, params, uri);
+
+ mFqdn = fqdn;
+ mClass = clazz;
+ mType = type;
+ }
+
+ public static String generateText (Context context, byte[] fingerprint) {
+
+ return String.format("pgpid+cookie=%s",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint));
+
+ }
+
+ public static DnsResource createNew (String domain) {
+ HashSet<String> flags = new HashSet<String>();
+ HashMap<String,String> params = new HashMap<String,String>();
+ URI uri = URI.create("dns:" + domain);
+ return create(flags, params, uri);
+ }
+
+ public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("dns".equals(uri.getScheme())
+ && (flags == null || flags.isEmpty())
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+
+ //
+ String spec = uri.getSchemeSpecificPart();
+ // If there are // at the beginning, this includes an authority - we don't support those!
+ if (spec.startsWith("//")) {
+ return null;
+ }
+
+ String[] pieces = spec.split("\\?", 2);
+ // In either case, part before a ? is the fqdn
+ String fqdn = pieces[0];
+ // There may be a query part
+ if (pieces.length > 1) {
+ // TODO parse CLASS and TYPE query paramters
+ }
+
+ CLASS clazz = CLASS.IN;
+ TYPE type = TYPE.TXT;
+
+ return new DnsResource(flags, params, uri, fqdn, clazz, type);
+ }
+
+ public String getFqdn() {
+ return mFqdn;
+ }
+
+ @Override
+ protected String fetchResource (OperationLog log, int indent) {
+
+ Client c = new Client();
+ DNSMessage msg = c.query(new Question(mFqdn, mType, mClass));
+ Record aw = msg.getAnswers()[0];
+ TXT txt = (TXT) aw.getPayload();
+ return txt.getText().toLowerCase();
+
+ }
+
+ @Override
+ protected Matcher matchResource(OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText() {
+ return R.string.linked_verified_dns;
+ }
+
+ @Override
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.dns;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return "Domain Name";
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mFqdn;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java
new file mode 100644
index 000000000..c6d5883ee
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java
@@ -0,0 +1,142 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import com.textuality.keybase.lib.Search;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.net.ssl.HttpsURLConnection;
+
+public class GenericHttpsResource extends LinkedCookieResource {
+
+ GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public static String generateText (Context context, byte[] fingerprint) {
+ String cookie = LinkedCookieResource.generate(context, fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_generic_text),
+ cookie, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
+ }
+
+ @Override
+ protected String fetchResource (OperationLog log, int indent) {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ indent += 1;
+
+ try {
+
+ HttpsURLConnection conn = null;
+ URL url = mSubUri.toURL();
+ int status = 0;
+ int redirects = 0;
+
+ while (redirects < 5) {
+ conn = (HttpsURLConnection) url.openConnection();
+ conn.addRequestProperty("User-Agent", "OpenKeychain");
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(25000);
+ conn.connect();
+ status = conn.getResponseCode();
+ if (status == 301) {
+ redirects++;
+ url = new URL(conn.getHeaderFields().get("Location").get(0));
+ log.add(LogType.MSG_LV_FETCH_REDIR, indent, url.toString());
+ } else {
+ break;
+ }
+ }
+
+ if (status >= 200 && status < 300) {
+ log.add(LogType.MSG_LV_FETCH_OK, indent, Integer.toString(status));
+ return Search.snarf(conn.getInputStream());
+ } else {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, Search.snarf(conn.getErrorStream()));
+ log.add(LogType.MSG_LV_FETCH_ERROR, indent, Integer.toString(status));
+ return null;
+ }
+
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, indent);
+ return null;
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ e.printStackTrace();
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, indent);
+ return null;
+ }
+
+ }
+
+ public static GenericHttpsResource createNew (URI uri) {
+ HashSet<String> flags = new HashSet<>();
+ flags.add("generic");
+ HashMap<String,String> params = new HashMap<>();
+ return create(flags, params, uri);
+ }
+
+ public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("https".equals(uri.getScheme())
+ && flags != null && flags.size() == 1 && flags.contains("generic")
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+ return new GenericHttpsResource(flags, params, uri);
+ }
+
+ @Override
+ public @DrawableRes
+ int getDisplayIcon() {
+ return R.drawable.ssl_lock;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText() {
+ return R.string.linked_verified_https;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return "Website (HTTPS)";
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mSubUri.toString();
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GithubResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GithubResource.java
new file mode 100644
index 000000000..400a0a678
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GithubResource.java
@@ -0,0 +1,218 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import org.apache.http.client.methods.HttpGet;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class GithubResource extends LinkedCookieResource {
+
+ final String mHandle;
+ final String mGistId;
+
+ GithubResource(Set<String> flags, HashMap<String,String> params, URI uri,
+ String handle, String gistId) {
+ super(flags, params, uri);
+
+ mHandle = handle;
+ mGistId = gistId;
+ }
+
+ public static String generate(Context context, byte[] fingerprint) {
+ String cookie = LinkedCookieResource.generate(context, fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_github_text), cookie);
+ }
+
+ @Override
+ protected String fetchResource (OperationLog log, int indent) {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ indent += 1;
+
+ try {
+
+ HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId);
+ httpGet.setHeader("User-Agent", "OpenKeychain");
+
+ String response = getResponseBody(httpGet);
+
+ JSONObject obj = new JSONObject(response);
+
+ JSONObject owner = obj.getJSONObject("owner");
+ if (!mHandle.equals(owner.getString("login"))) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, indent);
+ return null;
+ }
+
+ JSONObject files = obj.getJSONObject("files");
+ Iterator<String> it = files.keys();
+ if (it.hasNext()) {
+ // TODO can there be multiple candidates?
+ JSONObject file = files.getJSONObject(it.next());
+ return file.getString("content");
+ }
+
+ } catch (HttpStatusException e) {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
+ log.add(LogType.MSG_LV_FETCH_ERROR, indent, Integer.toString(e.getStatus()));
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, indent);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, indent);
+ } catch (JSONException e) {
+ Log.e(Constants.TAG, "json error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, indent);
+ }
+ return null;
+
+ }
+
+ public static GithubResource searchInGithubStream(String screenName, String needle) {
+
+ // narrow the needle down to important part
+ Matcher matcher = magicPattern.matcher(needle);
+ if (!matcher.find()) {
+ Log.e(Constants.TAG, "needle didn't contain cookie!");
+ return null;
+ }
+ needle = matcher.group();
+
+ try {
+
+ JSONArray array; {
+ HttpGet httpGet =
+ new HttpGet("https://api.github.com/users/" + screenName + "/gists");
+ httpGet.setHeader("Content-Type", "application/json");
+ httpGet.setHeader("User-Agent", "OpenKeychain");
+
+ String response = getResponseBody(httpGet);
+ array = new JSONArray(response);
+ }
+
+ for (int i = 0, j = Math.min(array.length(), 5); i < j; i++) {
+ JSONObject obj = array.getJSONObject(i);
+
+ JSONObject files = obj.getJSONObject("files");
+ Iterator<String> it = files.keys();
+ if (it.hasNext()) {
+
+ JSONObject file = files.getJSONObject(it.next());
+ String type = file.getString("type");
+ if (!"text/plain".equals(type)) {
+ continue;
+ }
+ String id = obj.getString("id");
+ HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id);
+ httpGet.setHeader("User-Agent", "OpenKeychain");
+
+ JSONObject gistObj = new JSONObject(getResponseBody(httpGet));
+ JSONObject gistFiles = gistObj.getJSONObject("files");
+ Iterator<String> gistIt = gistFiles.keys();
+ if (!gistIt.hasNext()) {
+ continue;
+ }
+ // TODO can there be multiple candidates?
+ JSONObject gistFile = gistFiles.getJSONObject(gistIt.next());
+ String content = gistFile.getString("content");
+ if (!content.contains(needle)) {
+ continue;
+ }
+
+ URI uri = URI.create("https://gist.github.com/" + screenName + "/" + id);
+ return create(uri);
+ }
+ }
+
+ // update the results with the body of the response
+ return null;
+ } catch (JSONException | HttpStatusException | IOException e) {
+ Log.e(Constants.TAG, "exception parsing stream", e);
+ }
+
+ return null;
+ }
+
+ public static GithubResource create(URI uri) {
+ return create(new HashSet<String>(), new HashMap<String,String>(), uri);
+ }
+
+ public static GithubResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+
+ // no params or flags
+ if (!flags.isEmpty() || !params.isEmpty()) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("https://gist\\.github\\.com/([a-zA-Z0-9_]+)/([0-9a-f]+)");
+ Matcher match = p.matcher(uri.toString());
+ if (!match.matches()) {
+ return null;
+ }
+ String handle = match.group(1);
+ String gistId = match.group(2);
+
+ return new GithubResource(flags, params, uri, handle, gistId);
+
+ }
+
+
+ @Override
+ public @DrawableRes
+ int getDisplayIcon() {
+ return R.drawable.github;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText() {
+ return R.string.linked_verified_github;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return "Github";
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mHandle;
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java
new file mode 100644
index 000000000..136d87d03
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java
@@ -0,0 +1,242 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+import android.util.Log;
+
+import com.textuality.keybase.lib.JWalk;
+
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TwitterResource extends LinkedCookieResource {
+
+ final String mHandle;
+ final String mTweetId;
+
+ TwitterResource(Set<String> flags, HashMap<String,String> params,
+ URI uri, String handle, String tweetId) {
+ super(flags, params, uri);
+
+ mHandle = handle;
+ mTweetId = tweetId;
+ }
+
+ public static TwitterResource create(URI uri) {
+ return create(new HashSet<String>(), new HashMap<String,String>(), uri);
+ }
+
+ public static TwitterResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+
+ // no params or flags
+ if (!flags.isEmpty() || !params.isEmpty()) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("https://twitter\\.com/([a-zA-Z0-9_]+)/status/([0-9]+)");
+ Matcher match = p.matcher(uri.toString());
+ if (!match.matches()) {
+ return null;
+ }
+ String handle = match.group(1);
+ String tweetId = match.group(2);
+
+ return new TwitterResource(flags, params, uri, handle, tweetId);
+
+ }
+
+ @Override
+ protected String fetchResource(OperationLog log, int indent) {
+
+ String authToken = getAuthToken();
+
+ if (authToken == null) {
+ return null;
+ }
+
+ HttpGet httpGet =
+ new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
+ + "?id=" + mTweetId
+ + "&include_entities=false");
+
+ // construct a normal HTTPS request and include an Authorization
+ // header with the value of Bearer <>
+ httpGet.setHeader("Authorization", "Bearer " + authToken);
+ httpGet.setHeader("Content-Type", "application/json");
+
+ try {
+ String response = getResponseBody(httpGet);
+ JSONObject obj = new JSONObject(response);
+
+ if (!obj.has("text")) {
+ return null;
+ }
+
+ JSONObject user = obj.getJSONObject("user");
+ if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, indent);
+ return null;
+ }
+
+ // update the results with the body of the response
+ return obj.getString("text");
+ } catch (HttpStatusException e) {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
+ log.add(LogType.MSG_LV_FETCH_ERROR, indent, Integer.toString(e.getStatus()));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, indent);
+ } catch (JSONException e) {
+ Log.e(Constants.TAG, "json error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, indent);
+ }
+
+ return null;
+ }
+
+ @Override
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.twitter;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText() {
+ return R.string.linked_verified_twitter;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return "Twitter";
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return "@" + mHandle;
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+
+ public static TwitterResource searchInTwitterStream(String screenName, String needle) {
+
+ String authToken = getAuthToken();
+
+ if (authToken == null) {
+ return null;
+ }
+
+ HttpGet httpGet =
+ new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ + "?screen_name=" + screenName
+ + "&count=15"
+ + "&include_rts=false"
+ + "&trim_user=true"
+ + "&exclude_replies=true");
+
+ // construct a normal HTTPS request and include an Authorization
+ // header with the value of Bearer <>
+ httpGet.setHeader("Authorization", "Bearer " + authToken);
+ httpGet.setHeader("Content-Type", "application/json");
+
+ try {
+ String response = getResponseBody(httpGet);
+ JSONArray array = new JSONArray(response);
+
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = array.getJSONObject(i);
+ String tweet = obj.getString("text");
+ if (tweet.contains(needle)) {
+ String id = obj.getString("id_str");
+ URI uri = URI.create("https://twitter.com/" + screenName + "/status/" + id);
+ return create(uri);
+ }
+ }
+
+ // update the results with the body of the response
+ return null;
+ } catch (JSONException | HttpStatusException | IOException e) {
+ Log.e(Constants.TAG, "exception parsing stream", e);
+ }
+
+ return null;
+ }
+
+ private static String authToken;
+
+ private static String getAuthToken() {
+ if (authToken != null) {
+ return authToken;
+ }
+ try {
+
+ String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ + "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
+
+ // Step 2: Obtain a bearer token
+ HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
+ httpPost.setHeader("Authorization", "Basic " + base64Encoded);
+ httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+ httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
+ JSONObject rawAuthorization = new JSONObject(getResponseBody(httpPost));
+
+ // Applications should verify that the value associated with the
+ // token_type key of the returned object is bearer
+ if (!"bearer".equals(JWalk.getString(rawAuthorization, "token_type"))) {
+ return null;
+ }
+
+ authToken = JWalk.getString(rawAuthorization, "access_token");
+ return authToken;
+
+ } catch (JSONException | IllegalStateException | HttpStatusException | IOException ex) {
+ Log.e(Constants.TAG, "exception fetching auth token", ex);
+ return null;
+ }
+
+ }
+
+ public static String rot13(String input) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c >= 'a' && c <= 'm') c += 13;
+ else if (c >= 'A' && c <= 'M') c += 13;
+ else if (c >= 'n' && c <= 'z') c -= 13;
+ else if (c >= 'N' && c <= 'Z') c -= 13;
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
index 6af5f4217..deb12a146 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -106,6 +106,7 @@ public class KeychainContract {
public static final String PATH_PUBLIC = "public";
public static final String PATH_SECRET = "secret";
public static final String PATH_USER_IDS = "user_ids";
+ public static final String PATH_LINKED_IDS = "linked_ids";
public static final String PATH_KEYS = "keys";
public static final String PATH_CERTS = "certs";
@@ -262,6 +263,11 @@ public class KeychainContract {
public static Uri buildUserIdsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
}
+
+ public static Uri buildLinkedIdsUri(Uri uri) {
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build();
+ }
+
}
public static class ApiApps implements ApiAppsColumns, BaseColumns {
@@ -350,7 +356,14 @@ public class KeychainContract {
}
public static Uri buildCertsUri(Uri uri) {
- return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).build();
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
+ .appendPath(PATH_CERTS).build();
+ }
+
+ public static Uri buildLinkedIdCertsUri(Uri uri, int rank) {
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
+ .appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank))
+ .appendPath(PATH_CERTS).build();
}
}
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 1351e0cbc..380c5cc46 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -31,6 +31,7 @@ import android.net.Uri;
import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -62,6 +63,8 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RING_SECRET = 204;
private static final int KEY_RING_CERTS = 205;
private static final int KEY_RING_CERTS_SPECIFIC = 206;
+ private static final int KEY_RING_LINKED_IDS = 207;
+ private static final int KEY_RING_LINKED_ID_CERTS = 208;
private static final int API_APPS = 301;
private static final int API_APPS_BY_PACKAGE_NAME = 302;
@@ -127,6 +130,9 @@ public class KeychainProvider extends ContentProvider {
* key_rings/_/unified
* key_rings/_/keys
* key_rings/_/user_ids
+ * key_rings/_/linked_ids
+ * key_rings/_/linked_ids/_
+ * key_rings/_/linked_ids/_/certs
* key_rings/_/public
* key_rings/_/secret
* key_rings/_/certs
@@ -143,6 +149,13 @@ public class KeychainProvider extends ContentProvider {
+ KeychainContract.PATH_USER_IDS,
KEY_RING_USER_IDS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ + KeychainContract.PATH_LINKED_IDS,
+ KEY_RING_LINKED_IDS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ + KeychainContract.PATH_LINKED_IDS + "/*/"
+ + KeychainContract.PATH_CERTS,
+ KEY_RING_LINKED_ID_CERTS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_PUBLIC,
KEY_RING_PUBLIC);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
@@ -477,7 +490,8 @@ public class KeychainProvider extends ContentProvider {
}
case KEY_RINGS_USER_IDS:
- case KEY_RING_USER_IDS: {
+ case KEY_RING_USER_IDS:
+ case KEY_RING_LINKED_IDS: {
HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id");
projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID);
@@ -502,13 +516,15 @@ public class KeychainProvider extends ContentProvider {
groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
+ ", " + Tables.USER_PACKETS + "." + UserPackets.RANK;
- // for now, we only respect user ids here, so TYPE must be NULL
- // TODO expand with KEY_RING_USER_PACKETS query type which lifts this restriction
- qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ if (match == KEY_RING_LINKED_IDS) {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = "
+ + WrappedUserAttribute.UAT_LINKED_ID);
+ } else {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ }
// If we are searching for a particular keyring's ids, add where
- if (match == KEY_RING_USER_IDS) {
- // TODO remove with the thing above
+ if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) {
qb.appendWhere(" AND ");
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
@@ -560,7 +576,8 @@ public class KeychainProvider extends ContentProvider {
}
case KEY_RING_CERTS:
- case KEY_RING_CERTS_SPECIFIC: {
+ case KEY_RING_CERTS_SPECIFIC:
+ case KEY_RING_LINKED_ID_CERTS: {
HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID);
projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID);
@@ -581,10 +598,6 @@ public class KeychainProvider extends ContentProvider {
+ " AND "
+ Tables.CERTS + "." + Certs.RANK + " = "
+ Tables.USER_PACKETS + "." + UserPackets.RANK
- // for now, we only return user ids here, so TYPE must be NULL
- // TODO at some point, we should lift this restriction
- + " AND "
- + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"
+ ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON ("
+ Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = "
+ "signer." + UserPackets.MASTER_KEY_ID
@@ -604,6 +617,17 @@ public class KeychainProvider extends ContentProvider {
qb.appendWhereEscapeString(uri.getPathSegments().get(4));
}
+ if (match == KEY_RING_LINKED_ID_CERTS) {
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ + UserPackets.TYPE + " IS NOT NULL");
+
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ + UserPackets.RANK + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(3));
+ } else {
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ }
+
break;
}
@@ -707,7 +731,7 @@ public class KeychainProvider extends ContentProvider {
)) {
throw new AssertionError("Incorrect type for user packet! This is a bug!");
}
- if (((Number)values.get(UserPacketsColumns.RANK)).intValue() == 0 && values.get(UserPacketsColumns.USER_ID) == null) {
+ if (values.get(UserPacketsColumns.RANK) == 0 && values.get(UserPacketsColumns.USER_ID) == null) {
throw new AssertionError("Rank 0 user packet must be a user id!");
}
db.insertOrThrow(Tables.USER_PACKETS, null, values);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
index 1d3934620..655bf19ba 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -31,10 +31,15 @@ import android.support.v4.util.LongSparseArray;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportExportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
-import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
@@ -50,7 +55,6 @@ import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
-import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -58,15 +62,11 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
-import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.remote.AppSettings;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
-import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
-import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.ProgressFixedScaler;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.Utf8Util;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
index f4b941109..57a11de21 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
@@ -23,6 +23,7 @@ import android.os.Parcelable;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.List;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
@@ -81,17 +82,11 @@ public class CertifyActionsParcel implements Parcelable {
final public ArrayList<String> mUserIds;
final public ArrayList<WrappedUserAttribute> mUserAttributes;
- public CertifyAction(long masterKeyId, ArrayList<String> userIds) {
+ public CertifyAction(long masterKeyId, List<String> userIds,
+ List<WrappedUserAttribute> attributes) {
mMasterKeyId = masterKeyId;
- mUserIds = userIds;
- mUserAttributes = null;
- }
-
- public CertifyAction(long masterKeyId, ArrayList<String> userIds,
- ArrayList<WrappedUserAttribute> attributes) {
- mMasterKeyId = masterKeyId;
- mUserIds = userIds;
- mUserAttributes = attributes;
+ mUserIds = userIds == null ? null : new ArrayList<>(userIds);
+ mUserAttributes = attributes == null ? null : new ArrayList<>(attributes);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
index d5f13f7ce..01b1925a0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
@@ -48,6 +48,14 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.DeleteResult;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.operations.results.CertifyResult;
+import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
+import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
+import org.sufficientlysecure.keychain.keyimport.Keyserver;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
index 61a02720e..86fb8dc80 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
@@ -329,7 +329,7 @@ public class EditKeyFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
case LOADER_ID_SUBKEYS: {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
index 360d30c82..b77637696 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
@@ -64,6 +64,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
*/
public class PassphraseDialogActivity extends FragmentActivity {
public static final String MESSAGE_DATA_PASSPHRASE = "passphrase";
+ public static final String EXTRA_KEY_ID = "key_id";
public static final String EXTRA_SUBKEY_ID = "secret_key_id";
@@ -238,6 +239,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
case DIVERT_TO_CARD:
message = getString(R.string.yubikey_pin_for, userId);
break;
+ // special case: empty passphrase just returns the empty passphrase
+ case PASSPHRASE_EMPTY:
+ finishCaching(new Passphrase(""));
default:
message = "This should not happen!";
break;
@@ -411,6 +415,8 @@ public class PassphraseDialogActivity extends FragmentActivity {
// also return passphrase back to activity
Intent returnIntent = new Intent();
returnIntent.putExtra(MESSAGE_DATA_PASSPHRASE, passphrase);
+ returnIntent.putExtra(EXTRA_KEY_ID, mSecretRing.getMasterKeyId());
+ returnIntent.putExtra(EXTRA_SUBKEY_ID, mSubKeyId);
getActivity().setResult(RESULT_OK, returnIntent);
}
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 484956e6a..435a455dc 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -35,6 +35,7 @@ import android.os.Message;
import android.os.Messenger;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
@@ -64,6 +65,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler.MessageStatus;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
@@ -255,7 +257,6 @@ public class ViewKeyActivity extends BaseActivity implements
mNfcHelper = new NfcHelper(this, mProviderHelper);
mNfcHelper.initNfc(mDataUri);
- startFragment(savedInstanceState, mDataUri);
}
@Override
@@ -263,26 +264,6 @@ public class ViewKeyActivity extends BaseActivity implements
setContentView(R.layout.view_key_activity);
}
- private void startFragment(Bundle savedInstanceState, Uri dataUri) {
- // However, if we're being restored from a previous state,
- // then we don't need to do anything and should return or else
- // we could end up with overlapping fragments.
- if (savedInstanceState != null) {
- return;
- }
-
- // Create an instance of the fragment
- ViewKeyFragment frag = ViewKeyFragment.newInstance(dataUri);
-
- // Add the fragment to the 'fragment_container' FrameLayout
- // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.view_key_fragment, frag)
- .commitAllowingStateLoss();
- // do it immediately!
- getSupportFragmentManager().executePendingTransactions();
- }
-
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
@@ -342,6 +323,12 @@ public class ViewKeyActivity extends BaseActivity implements
}
return true;
}
+ case R.id.menu_key_view_add_linked_identity: {
+ Intent intent = new Intent(this, LinkedIdWizard.class);
+ intent.setData(mDataUri);
+ startActivity(intent);
+ return true;
+ }
case R.id.menu_key_view_edit: {
editKey(mDataUri);
return true;
@@ -358,6 +345,10 @@ public class ViewKeyActivity extends BaseActivity implements
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret);
+
+ MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
+ addLinked.setVisible(mIsSecret);
+
MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint);
certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked);
@@ -715,166 +706,173 @@ public class ViewKeyActivity extends BaseActivity implements
// old cursor once we return.)
switch (loader.getId()) {
case LOADER_ID_UNIFIED: {
- if (data.moveToFirst()) {
- // get name, email, and comment from USER_ID
- KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID));
- if (mainUserId.name != null) {
+ // if there is no data, just break
+ if (!data.moveToFirst()) {
+ break;
+ }
+
+ String oldFingerprint = mFingerprint;
+ mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
+ byte[] fpData = data.getBlob(INDEX_FINGERPRINT);
+ mFingerprint = KeyFormattingUtils.convertFingerprintToHex(fpData);
+
+ mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
+ mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0;
+ mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0;
+ mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0;
+ mIsVerified = data.getInt(INDEX_VERIFIED) > 0;
+
+ startFragment(mIsSecret, fpData);
+
+ // get name, email, and comment from USER_ID
+ KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID));
+ if (mainUserId.name != null) {
+ mName.setText(mainUserId.name);
+ } else {
+ mName.setText(R.string.user_id_no_name);
+ }
+
+ // if the refresh animation isn't playing
+ if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) {
+ // re-create options menu based on mIsSecret, mIsVerified
+ supportInvalidateOptionsMenu();
+ // this is done at the end of the animation otherwise
+ }
+
+ AsyncTask<Long, Void, Bitmap> photoTask =
+ new AsyncTask<Long, Void, Bitmap>() {
+ protected Bitmap doInBackground(Long... mMasterKeyId) {
+ return ContactHelper.loadPhotoByMasterKeyId(getContentResolver(), mMasterKeyId[0], true);
+ }
+
+ protected void onPostExecute(Bitmap photo) {
+ mPhoto.setImageBitmap(photo);
+ mPhoto.setVisibility(View.VISIBLE);
+ }
+ };
+
+ // Note: order is important
+ int color;
+ if (mIsRevoked) {
+ mStatusText.setText(R.string.view_key_revoked);
+ mStatusImage.setVisibility(View.VISIBLE);
+ KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
+ State.REVOKED, R.color.icons, true);
+ color = getResources().getColor(R.color.android_red_light);
+
+ mActionEncryptFile.setVisibility(View.GONE);
+ mActionEncryptText.setVisibility(View.GONE);
+ mActionNfc.setVisibility(View.GONE);
+ mFab.setVisibility(View.GONE);
+ mQrCodeLayout.setVisibility(View.GONE);
+ } else if (mIsExpired) {
+ if (mIsSecret) {
+ mStatusText.setText(R.string.view_key_expired_secret);
mName.setText(mainUserId.name);
} else {
- mName.setText(R.string.user_id_no_name);
+ mStatusText.setText(R.string.view_key_expired);
}
+ mStatusImage.setVisibility(View.VISIBLE);
+ KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
+ State.EXPIRED, R.color.icons, true);
+ color = getResources().getColor(R.color.android_red_light);
+
+ mActionEncryptFile.setVisibility(View.GONE);
+ mActionEncryptText.setVisibility(View.GONE);
+ mActionNfc.setVisibility(View.GONE);
+ mFab.setVisibility(View.GONE);
+ mQrCodeLayout.setVisibility(View.GONE);
+ } else if (mIsSecret) {
+ mStatusText.setText(R.string.view_key_my_key);
+ mStatusImage.setVisibility(View.GONE);
+ color = getResources().getColor(R.color.primary);
+ // reload qr code only if the fingerprint changed
+ if (!mFingerprint.equals(oldFingerprint)) {
+ loadQrCode(mFingerprint);
+ }
+ photoTask.execute(mMasterKeyId);
+ mQrCodeLayout.setVisibility(View.VISIBLE);
+
+ // and place leftOf qr code
+ RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams)
+ mName.getLayoutParams();
+ // remove right margin
+ nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ nameParams.setMarginEnd(0);
+ }
+ nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
+ mName.setLayoutParams(nameParams);
+
+ RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams)
+ mStatusText.getLayoutParams();
+ statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ statusParams.setMarginEnd(0);
+ }
+ statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
+ mStatusText.setLayoutParams(statusParams);
+
+ mActionEncryptFile.setVisibility(View.VISIBLE);
+ mActionEncryptText.setVisibility(View.VISIBLE);
- String oldFingerprint = mFingerprint;
- mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
- mFingerprint = KeyFormattingUtils.convertFingerprintToHex(data.getBlob(INDEX_FINGERPRINT));
-
- mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
- mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0;
- mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0;
- mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0;
- mIsVerified = data.getInt(INDEX_VERIFIED) > 0;
-
- // if the refresh animation isn't playing
- if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) {
- // re-create options menu based on mIsSecret, mIsVerified
- supportInvalidateOptionsMenu();
- // this is done at the end of the animation otherwise
+ // invokeBeam is available from API 21
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mActionNfc.setVisibility(View.VISIBLE);
+ } else {
+ mActionNfc.setVisibility(View.GONE);
}
+ mFab.setVisibility(View.VISIBLE);
+ mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp));
+ } else {
+ mActionEncryptFile.setVisibility(View.VISIBLE);
+ mActionEncryptText.setVisibility(View.VISIBLE);
+ mQrCodeLayout.setVisibility(View.GONE);
+ mActionNfc.setVisibility(View.GONE);
- AsyncTask<Long, Void, Bitmap> photoTask =
- new AsyncTask<Long, Void, Bitmap>() {
- protected Bitmap doInBackground(Long... mMasterKeyId) {
- return ContactHelper.loadPhotoByMasterKeyId(getContentResolver(), mMasterKeyId[0], true);
- }
-
- protected void onPostExecute(Bitmap photo) {
- mPhoto.setImageBitmap(photo);
- mPhoto.setVisibility(View.VISIBLE);
- }
- };
-
- // Note: order is important
- int color;
- if (mIsRevoked) {
- mStatusText.setText(R.string.view_key_revoked);
+ if (mIsVerified) {
+ mStatusText.setText(R.string.view_key_verified);
mStatusImage.setVisibility(View.VISIBLE);
KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
- State.REVOKED, R.color.icons, true);
- color = getResources().getColor(R.color.android_red_light);
+ State.VERIFIED, R.color.icons, true);
+ color = getResources().getColor(R.color.primary);
+ photoTask.execute(mMasterKeyId);
- mActionEncryptFile.setVisibility(View.GONE);
- mActionEncryptText.setVisibility(View.GONE);
- mActionNfc.setVisibility(View.GONE);
mFab.setVisibility(View.GONE);
- mQrCodeLayout.setVisibility(View.GONE);
- } else if (mIsExpired) {
- if (mIsSecret) {
- mStatusText.setText(R.string.view_key_expired_secret);
- } else {
- mStatusText.setText(R.string.view_key_expired);
- }
+ } else {
+ mStatusText.setText(R.string.view_key_unverified);
mStatusImage.setVisibility(View.VISIBLE);
KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
- State.EXPIRED, R.color.icons, true);
- color = getResources().getColor(R.color.android_red_light);
+ State.UNVERIFIED, R.color.icons, true);
+ color = getResources().getColor(R.color.android_orange_light);
- mActionEncryptFile.setVisibility(View.GONE);
- mActionEncryptText.setVisibility(View.GONE);
- mActionNfc.setVisibility(View.GONE);
- mFab.setVisibility(View.GONE);
- mQrCodeLayout.setVisibility(View.GONE);
- } else if (mIsSecret) {
- mStatusText.setText(R.string.view_key_my_key);
- mStatusImage.setVisibility(View.GONE);
- color = getResources().getColor(R.color.primary);
- // reload qr code only if the fingerprint changed
- if (!mFingerprint.equals(oldFingerprint)) {
- loadQrCode(mFingerprint);
- }
- photoTask.execute(mMasterKeyId);
- mQrCodeLayout.setVisibility(View.VISIBLE);
-
- // and place leftOf qr code
- RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams)
- mName.getLayoutParams();
- // remove right margin
- nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- nameParams.setMarginEnd(0);
- }
- nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
- mName.setLayoutParams(nameParams);
-
- RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams)
- mStatusText.getLayoutParams();
- statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- statusParams.setMarginEnd(0);
- }
- statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
- mStatusText.setLayoutParams(statusParams);
-
- mActionEncryptFile.setVisibility(View.VISIBLE);
- mActionEncryptText.setVisibility(View.VISIBLE);
-
- // invokeBeam is available from API 21
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- mActionNfc.setVisibility(View.VISIBLE);
- } else {
- mActionNfc.setVisibility(View.GONE);
- }
mFab.setVisibility(View.VISIBLE);
- mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp));
- } else {
- mActionEncryptFile.setVisibility(View.VISIBLE);
- mActionEncryptText.setVisibility(View.VISIBLE);
- mQrCodeLayout.setVisibility(View.GONE);
- mActionNfc.setVisibility(View.GONE);
-
- if (mIsVerified) {
- mStatusText.setText(R.string.view_key_verified);
- mStatusImage.setVisibility(View.VISIBLE);
- KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
- State.VERIFIED, R.color.icons, true);
- color = getResources().getColor(R.color.primary);
- photoTask.execute(mMasterKeyId);
-
- mFab.setVisibility(View.GONE);
- } else {
- mStatusText.setText(R.string.view_key_unverified);
- mStatusImage.setVisibility(View.VISIBLE);
- KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText,
- State.UNVERIFIED, R.color.icons, true);
- color = getResources().getColor(R.color.android_orange_light);
-
- mFab.setVisibility(View.VISIBLE);
- }
}
+ }
- if (mPreviousColor == 0 || mPreviousColor == color) {
- mStatusBar.setBackgroundColor(color);
- mBigToolbar.setBackgroundColor(color);
- mPreviousColor = color;
- } else {
- ObjectAnimator colorFade1 =
- ObjectAnimator.ofObject(mStatusBar, "backgroundColor",
- new ArgbEvaluator(), mPreviousColor, color);
- ObjectAnimator colorFade2 =
- ObjectAnimator.ofObject(mBigToolbar, "backgroundColor",
- new ArgbEvaluator(), mPreviousColor, color);
-
- colorFade1.setDuration(1200);
- colorFade2.setDuration(1200);
- colorFade1.start();
- colorFade2.start();
- mPreviousColor = color;
- }
+ if (mPreviousColor == 0 || mPreviousColor == color) {
+ mStatusBar.setBackgroundColor(color);
+ mBigToolbar.setBackgroundColor(color);
+ mPreviousColor = color;
+ } else {
+ ObjectAnimator colorFade1 =
+ ObjectAnimator.ofObject(mStatusBar, "backgroundColor",
+ new ArgbEvaluator(), mPreviousColor, color);
+ ObjectAnimator colorFade2 =
+ ObjectAnimator.ofObject(mBigToolbar, "backgroundColor",
+ new ArgbEvaluator(), mPreviousColor, color);
+
+ colorFade1.setDuration(1200);
+ colorFade2.setDuration(1200);
+ colorFade1.start();
+ colorFade2.start();
+ mPreviousColor = color;
+ }
- //noinspection deprecation
- mStatusImage.setAlpha(80);
+ //noinspection deprecation
+ mStatusImage.setAlpha(80);
- break;
- }
+ break;
}
}
}
@@ -883,4 +881,28 @@ public class ViewKeyActivity extends BaseActivity implements
public void onLoaderReset(Loader<Cursor> loader) {
}
-} \ No newline at end of file
+
+ private void startFragment(final boolean isSecret, final byte[] fingerprint) {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+
+ FragmentManager manager = getSupportFragmentManager();
+ if (manager.getBackStackEntryCount() == 0) {
+ // Create an instance of the fragment
+ final ViewKeyFragment frag = ViewKeyFragment.newInstance(
+ mDataUri, isSecret, fingerprint);
+ manager.beginTransaction()
+ .replace(R.id.view_key_fragment, frag)
+ .commit();
+ manager.popBackStack();
+ } else {
+ // not sure yet if we actually want this!
+ // manager.popBackStack();
+ }
+
+ }
+ });
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
index 7bfebaf62..ad437f924 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
@@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
default:
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
index c3a8d60f8..8610b2fee 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
@@ -18,29 +18,41 @@
package org.sufficientlysecure.keychain.ui;
+import java.io.IOException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.provider.ContactsContract;
import android.support.v4.app.LoaderManager;
-import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
+import android.support.v7.widget.CardView;
+import android.transition.Fade;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
import android.widget.*;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
-import org.sufficientlysecure.keychain.pgp.KeyRing;
-import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.ContactHelper;
import org.sufficientlysecure.keychain.util.Log;
@@ -50,6 +62,8 @@ public class ViewKeyFragment extends LoaderFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_DATA_URI = "uri";
+ private static final String ARG_FINGERPRINT = "fingerprint";
+ private static final String ARG_IS_SECRET = "is_secret";
private ListView mUserIds;
//private ListView mLinkedSystemContact;
@@ -61,20 +75,27 @@ public class ViewKeyFragment extends LoaderFragment implements
ImageView mSystemContactPicture;
TextView mSystemContactName;
- private static final int LOADER_ID_UNIFIED = 0;
- private static final int LOADER_ID_USER_IDS = 1;
+ private static final int LOADER_ID_USER_IDS = 0;
+ private static final int LOADER_ID_LINKED_IDS = 1;
private UserIdsAdapter mUserIdsAdapter;
+ private LinkedIdsAdapter mLinkedIdsAdapter;
private Uri mDataUri;
+ private ListView mLinkedIds;
+ private CardView mLinkedIdsCard;
+ private byte[] mFingerprint;
+ private TextView mLinkedIdsExpander;
/**
* Creates new instance of this fragment
*/
- public static ViewKeyFragment newInstance(Uri dataUri) {
+ public static ViewKeyFragment newInstance(Uri dataUri, boolean isSecret, byte[] fingerprint) {
ViewKeyFragment frag = new ViewKeyFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putBoolean(ARG_IS_SECRET, isSecret);
+ args.putByteArray(ARG_FINGERPRINT, fingerprint);
frag.setArguments(args);
@@ -82,11 +103,33 @@ public class ViewKeyFragment extends LoaderFragment implements
}
@Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Bundle args = getArguments();
+ Uri dataUri = args.getParcelable(ARG_DATA_URI);
+ if (dataUri == null) {
+ Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
+ getActivity().finish();
+ return;
+ }
+ boolean isSecret = args.getBoolean(ARG_IS_SECRET);
+ byte[] fingerprint = args.getByteArray(ARG_FINGERPRINT);
+
+ loadData(dataUri, isSecret, fingerprint);
+ }
+
+ @Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.view_key_fragment, getContainer());
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
+ mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids);
+
+ mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids);
+
+ mLinkedIdsExpander = (TextView) view.findViewById(R.id.view_key_linked_ids_expander);
mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -94,6 +137,12 @@ public class ViewKeyFragment extends LoaderFragment implements
showUserIdInfo(position);
}
});
+ mLinkedIds.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ showLinkedId(position);
+ }
+ });
mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout);
mSystemContactName = (TextView) view.findViewById(R.id.system_contact_name);
@@ -102,6 +151,47 @@ public class ViewKeyFragment extends LoaderFragment implements
return root;
}
+ private void showLinkedId(final int position) {
+ final LinkedIdViewFragment frag;
+ try {
+ frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Transition trans = TransitionInflater.from(getActivity())
+ .inflateTransition(R.transition.linked_id_card_trans);
+ // setSharedElementReturnTransition(trans);
+ setExitTransition(new Fade());
+ frag.setSharedElementEnterTransition(trans);
+ }
+
+ getFragmentManager().beginTransaction()
+ .add(R.id.view_key_fragment, frag)
+ .hide(frag)
+ .commit();
+
+ frag.setOnIdentityLoadedListener(new OnIdentityLoadedListener() {
+ @Override
+ public void onIdentityLoaded() {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().beginTransaction()
+ .show(frag)
+ .addSharedElement(mLinkedIdsCard, "card_linked_ids")
+ .remove(ViewKeyFragment.this)
+ .addToBackStack("linked_id")
+ .commit();
+ }
+ });
+ }
+ });
+
+ }
+
private void showUserIdInfo(final int position) {
if (!mIsSecret) {
final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position);
@@ -181,52 +271,23 @@ public class ViewKeyFragment extends LoaderFragment implements
context.startActivity(intent);
}
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- Uri dataUri = getArguments().getParcelable(ARG_DATA_URI);
- if (dataUri == null) {
- Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
- getActivity().finish();
- return;
- }
-
- loadData(dataUri);
- }
-
-
- // These are the rows that we will retrieve.
- static final String[] UNIFIED_PROJECTION = new String[]{
- KeychainContract.KeyRings._ID,
- KeychainContract.KeyRings.MASTER_KEY_ID,
- KeychainContract.KeyRings.USER_ID,
- KeychainContract.KeyRings.IS_REVOKED,
- KeychainContract.KeyRings.IS_EXPIRED,
- KeychainContract.KeyRings.VERIFIED,
- KeychainContract.KeyRings.HAS_ANY_SECRET,
- KeychainContract.KeyRings.FINGERPRINT,
- KeychainContract.KeyRings.HAS_ENCRYPT
- };
-
- static final int INDEX_MASTER_KEY_ID = 1;
- static final int INDEX_USER_ID = 2;
- static final int INDEX_IS_REVOKED = 3;
- static final int INDEX_IS_EXPIRED = 4;
- static final int INDEX_VERIFIED = 5;
- static final int INDEX_HAS_ANY_SECRET = 6;
- static final int INDEX_FINGERPRINT = 7;
- static final int INDEX_HAS_ENCRYPT = 8;
-
- private void loadData(Uri dataUri) {
+ private void loadData(Uri dataUri, boolean isSecret, byte[] fingerprint) {
mDataUri = dataUri;
+ mIsSecret = isSecret;
+ mFingerprint = fingerprint;
Log.i(Constants.TAG, "mDataUri: " + mDataUri);
- // Prepare the loaders. Either re-connect with an existing ones,
- // or start new ones.
- // TODO Is this loader the same as the one in the activity?
- getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
+ // load user ids after we know if it's a secret key
+ mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null);
+ mUserIds.setAdapter(mUserIdsAdapter);
+ getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
+
+ mLinkedIdsAdapter = new LinkedIdsAdapter(getActivity(), null, 0,
+ mIsSecret, mLinkedIdsExpander);
+ mLinkedIds.setAdapter(mLinkedIdsAdapter);
+ getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this);
+
}
@Override
@@ -234,54 +295,33 @@ public class ViewKeyFragment extends LoaderFragment implements
setContentShown(false);
switch (id) {
- case LOADER_ID_UNIFIED: {
- Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri);
- return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null);
- }
case LOADER_ID_USER_IDS:
return UserIdsAdapter.createLoader(getActivity(), mDataUri);
+ case LOADER_ID_LINKED_IDS:
+ return LinkedIdsAdapter.createLoader(getActivity(), mDataUri);
+
default:
return null;
}
}
@Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- /* TODO better error handling? May cause problems when a key is deleted,
- * because the notification triggers faster than the activity closes.
- */
- // Avoid NullPointerExceptions...
- if (data.getCount() == 0) {
- return;
- }
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
switch (loader.getId()) {
- case LOADER_ID_UNIFIED: {
- if (data.moveToFirst()) {
-
- mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
-
- //TODO system to allow immediate refreshing of system contact on verification
- if (!mSystemContactLoaded) {//ensure we load linked system contact only once
- long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
- loadLinkedSystemContact(masterKeyId);
- }
- // load user ids after we know if it's a secret key
- mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null);
- mUserIds.setAdapter(mUserIdsAdapter);
- getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
-
- break;
- }
- }
-
case LOADER_ID_USER_IDS: {
- mUserIdsAdapter.swapCursor(data);
+ mUserIdsAdapter.swapCursor(cursor);
+ loadLinkedSystemContact(KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint));
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
+ mLinkedIdsAdapter.swapCursor(cursor);
+ break;
+ }
}
setContentShown(true);
}
@@ -297,6 +337,11 @@ public class ViewKeyFragment extends LoaderFragment implements
mUserIdsAdapter.swapCursor(null);
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsCard.setVisibility(View.GONE);
+ mLinkedIdsAdapter.swapCursor(null);
+ break;
+ }
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
new file mode 100644
index 000000000..e94ea5189
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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.adapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.v4.content.CursorLoader;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+import org.sufficientlysecure.keychain.util.FilterCursorWrapper;
+
+import java.io.IOException;
+import java.util.WeakHashMap;
+
+public class LinkedIdsAdapter extends UserAttributesAdapter {
+ private final boolean mIsSecret;
+ protected LayoutInflater mInflater;
+ WeakHashMap<Integer,RawLinkedIdentity> mLinkedIdentityCache = new WeakHashMap<>();
+
+ private Cursor mUnfilteredCursor;
+
+ private TextView mExpander;
+
+ public LinkedIdsAdapter(Context context, Cursor c, int flags,
+ boolean isSecret, TextView expander) {
+ super(context, c, flags);
+ mInflater = LayoutInflater.from(context);
+ mIsSecret = isSecret;
+
+ if (expander != null) {
+ expander.setVisibility(View.GONE);
+ /* don't show an expander (maybe in some sort of advanced view?)
+ mExpander = expander;
+ mExpander.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showUnfiltered();
+ }
+ });
+ */
+ }
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor cursor) {
+ if (cursor == null) {
+ mUnfilteredCursor = null;
+ return super.swapCursor(null);
+ }
+ mUnfilteredCursor = cursor;
+ FilterCursorWrapper filteredCursor = new FilterCursorWrapper(cursor) {
+ @Override
+ public boolean isVisible(Cursor cursor) {
+ RawLinkedIdentity id = getItemAtPosition(cursor);
+ return id instanceof LinkedIdentity;
+ }
+ };
+
+ if (mExpander != null) {
+ int hidden = filteredCursor.getHiddenCount();
+ if (hidden == 0) {
+ mExpander.setVisibility(View.GONE);
+ } else {
+ mExpander.setVisibility(View.VISIBLE);
+ mExpander.setText(mContext.getResources().getQuantityString(
+ R.plurals.linked_id_expand, hidden));
+ }
+ }
+
+ return super.swapCursor(filteredCursor);
+ }
+
+ private void showUnfiltered() {
+ mExpander.setVisibility(View.GONE);
+ super.swapCursor(mUnfilteredCursor);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ if (!mIsSecret) {
+ holder.vVerified.setVisibility(View.VISIBLE);
+ int isVerified = cursor.getInt(INDEX_VERIFIED);
+ switch (isVerified) {
+ case Certs.VERIFIED_SECRET:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ case Certs.VERIFIED_SELF:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ default:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ }
+ } else {
+ holder.vVerified.setVisibility(View.GONE);
+ }
+
+ RawLinkedIdentity id = getItemAtPosition(cursor);
+ holder.setData(mContext, id);
+
+ }
+
+ public RawLinkedIdentity getItemAtPosition(Cursor cursor) {
+ int rank = cursor.getInt(INDEX_RANK);
+ Log.d(Constants.TAG, "requested rank: " + rank);
+
+ RawLinkedIdentity ret = mLinkedIdentityCache.get(rank);
+ if (ret != null) {
+ Log.d(Constants.TAG, "cached!");
+ return ret;
+ }
+ Log.d(Constants.TAG, "not cached!");
+
+ try {
+ byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA);
+ ret = LinkedIdentity.fromAttributeData(data);
+ mLinkedIdentityCache.put(rank, ret);
+ return ret;
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "could not read linked identity subpacket data", e);
+ return null;
+ }
+ }
+
+ @Override
+ public RawLinkedIdentity getItem(int position) {
+ Cursor cursor = getCursor();
+ cursor.moveToPosition(position);
+ return getItemAtPosition(cursor);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View v = mInflater.inflate(R.layout.linked_id_item, null);
+ ViewHolder holder = new ViewHolder(v);
+ v.setTag(holder);
+ return v;
+ }
+
+ // don't show revoked user ids, irrelevant for average users
+ public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
+
+ public static CursorLoader createLoader(Activity activity, Uri dataUri) {
+ Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
+ return new CursorLoader(activity, baseUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null);
+ }
+
+ public LinkedIdViewFragment getLinkedIdFragment(Uri baseUri,
+ int position, byte[] fingerprint) throws IOException {
+ Cursor c = getCursor();
+ c.moveToPosition(position);
+ int rank = c.getInt(UserIdsAdapter.INDEX_RANK);
+
+ Uri dataUri = UserPackets.buildLinkedIdsUri(baseUri);
+ return LinkedIdViewFragment.newInstance(dataUri, rank, mIsSecret, fingerprint);
+ }
+
+ public static class ViewHolder {
+ final public ImageView vVerified;
+ final public ImageView vIcon;
+ final public TextView vTitle;
+ final public TextView vComment;
+
+ public ViewHolder(View view) {
+ vVerified = (ImageView) view.findViewById(R.id.linked_id_certified_icon);
+ vIcon = (ImageView) view.findViewById(R.id.linked_id_type_icon);
+ vTitle = (TextView) view.findViewById(R.id.linked_id_title);
+ vComment = (TextView) view.findViewById(R.id.linked_id_comment);
+ }
+
+ public void setData(Context context, RawLinkedIdentity id) {
+
+ vTitle.setText(id.getDisplayTitle(context));
+
+ String comment = id.getDisplayComment(context);
+ if (comment != null) {
+ vComment.setVisibility(View.VISIBLE);
+ vComment.setText(comment);
+ } else {
+ vComment.setVisibility(View.GONE);
+ }
+
+ vIcon.setImageResource(id.getDisplayIcon());
+
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java
new file mode 100644
index 000000000..5ecd9f408
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java
@@ -0,0 +1,57 @@
+package org.sufficientlysecure.keychain.ui.adapter;
+
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.v4.content.CursorLoader;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+
+
+public class LinkedIdsCertAdapter extends CursorAdapter {
+
+ public static final String[] USER_CERTS_PROJECTION = new String[]{
+ UserPackets._ID,
+ UserPackets.TYPE,
+ UserPackets.USER_ID,
+ UserPackets.ATTRIBUTE_DATA,
+ UserPackets.RANK,
+ UserPackets.VERIFIED,
+ UserPackets.IS_PRIMARY,
+ UserPackets.IS_REVOKED
+ };
+ protected static final int INDEX_ID = 0;
+ protected static final int INDEX_TYPE = 1;
+ protected static final int INDEX_USER_ID = 2;
+ protected static final int INDEX_ATTRIBUTE_DATA = 3;
+ protected static final int INDEX_RANK = 4;
+ protected static final int INDEX_VERIFIED = 5;
+ protected static final int INDEX_IS_PRIMARY = 6;
+ protected static final int INDEX_IS_REVOKED = 7;
+
+ public LinkedIdsCertAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return null;
+ }
+
+ public static CursorLoader createLoader(Activity activity, Uri dataUri) {
+ Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
+ return new CursorLoader(activity, baseUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
index 028f0fc9c..5218273a0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
@@ -168,7 +168,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
CertifyAction action = actions.get(keyId);
if (actions.get(keyId) == null) {
- actions.put(keyId, new CertifyAction(keyId, uids));
+ actions.put(keyId, new CertifyAction(keyId, uids, null));
} else {
action.mUserIds.addAll(uids);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
index 457083770..e0abaf4b0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
@@ -8,22 +8,24 @@ import android.view.View;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public abstract class UserAttributesAdapter extends CursorAdapter {
- public static final String[] USER_IDS_PROJECTION = new String[]{
+ public static final String[] USER_PACKETS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
+ UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
- protected static final int INDEX_ID = 0;
- protected static final int INDEX_TYPE = 1;
- protected static final int INDEX_USER_ID = 2;
- protected static final int INDEX_RANK = 3;
- protected static final int INDEX_VERIFIED = 4;
- protected static final int INDEX_IS_PRIMARY = 5;
- protected static final int INDEX_IS_REVOKED = 6;
+ public static final int INDEX_ID = 0;
+ public static final int INDEX_TYPE = 1;
+ public static final int INDEX_USER_ID = 2;
+ public static final int INDEX_ATTRIBUTE_DATA = 3;
+ public static final int INDEX_RANK = 4;
+ public static final int INDEX_VERIFIED = 5;
+ public static final int INDEX_IS_PRIMARY = 6;
+ public static final int INDEX_IS_REVOKED = 7;
public UserAttributesAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
mCursor.moveToPosition(position);
return mCursor.getInt(INDEX_VERIFIED);
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
index d1103ac1f..c68c078ad 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
@@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter {
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildUserIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java
new file mode 100644
index 000000000..6b3eef26a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java
@@ -0,0 +1,132 @@
+/*
+ * 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.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+
+public class LinkedIdCreateDnsStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditDns;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateDnsStep1Fragment newInstance() {
+ LinkedIdCreateDnsStep1Fragment frag = new LinkedIdCreateDnsStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_dns_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String uri = mEditDns.getText().toString();
+
+ if (!checkUri(uri)) {
+ mEditDns.setError("Please enter a valid domain name!");
+ return;
+ }
+
+ String proofText = DnsResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint);
+
+ LinkedIdCreateDnsStep2Fragment frag =
+ LinkedIdCreateDnsStep2Fragment.newInstance(uri, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditDns = (EditText) view.findViewById(R.id.linked_create_dns_domain);
+
+ mEditDns.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String uri = editable.toString();
+ if (uri.length() > 0) {
+ if (checkUri(uri)) {
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_ok, 0);
+ } else {
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_bad, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ });
+
+ mEditDns.setText("test.mugenguild.com");
+
+ return view;
+ }
+
+ private static boolean checkUri(String uri) {
+ return Patterns.DOMAIN_NAME.matcher(uri).matches();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java
new file mode 100644
index 000000000..c0e2fd29c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java
@@ -0,0 +1,154 @@
+/*
+ * 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.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+
+public class LinkedIdCreateDnsStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+
+ public static final String DOMAIN = "domain", TEXT = "text";
+
+ TextView mTextView;
+
+ String mResourceDomain;
+ String mResourceString;
+
+ public static LinkedIdCreateDnsStep2Fragment newInstance
+ (String uri, String proofText) {
+
+ LinkedIdCreateDnsStep2Fragment frag = new LinkedIdCreateDnsStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(DOMAIN, uri);
+ args.putString(TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mResourceDomain = getArguments().getString(DOMAIN);
+ mResourceString = getArguments().getString(TEXT);
+
+ }
+
+ @Override
+ protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_dns_fragment_step2, container, false);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofToClipboard();
+ }
+ });
+
+ mTextView = (TextView) view.findViewById(R.id.linked_create_dns_text);
+ mTextView.setText(mResourceString);
+
+ return view;
+ }
+
+ @Override
+ LinkedCookieResource getResource() {
+ return DnsResource.createNew(mResourceDomain);
+ }
+
+ private void proofSend () {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofToClipboard() {
+ ClipboardReflection.copyToClipboard(getActivity(), mResourceString);
+ Notify.create(getActivity(), R.string.linked_text_clipboard, Notify.Style.OK).show();
+ }
+
+ private void saveFile(Uri uri) {
+ try {
+ PrintWriter out =
+ new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
+ out.print(mResourceString);
+ if (out.checkError()) {
+ Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
+ }
+ } catch (FileNotFoundException e) {
+ Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // For saving a file
+ case REQUEST_CODE_OUTPUT:
+ if (data == null) {
+ return;
+ }
+ Uri uri = data.getData();
+ saveFile(uri);
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java
new file mode 100644
index 000000000..99e770857
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java
@@ -0,0 +1,262 @@
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+public abstract class LinkedIdCreateFinalFragment extends Fragment {
+
+ protected static final int REQUEST_CODE_PASSPHRASE = 0x00007008;
+
+ protected LinkedIdWizard mLinkedIdWizard;
+
+ private ImageView mVerifyImage;
+ private TextView mVerifyStatus;
+ private ViewAnimator mVerifyAnimator;
+
+ // This is a resource, set AFTER it has been verified
+ LinkedCookieResource mVerifiedResource = null;
+ private ViewAnimator mVerifyButtonAnimator;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+ protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = newView(inflater, container, savedInstanceState);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startCertify();
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mVerifyAnimator = (ViewAnimator) view.findViewById(R.id.verify_progress);
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+ mVerifyButtonAnimator = (ViewAnimator) view.findViewById(R.id.verify_buttons);
+
+ view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ setVerifyProgress(false, null);
+ mVerifyStatus.setText(R.string.linked_verify_pending);
+
+ return view;
+ }
+
+ abstract LinkedCookieResource getResource();
+
+ private void setVerifyProgress(boolean on, Boolean success) {
+ if (success == null) {
+ mVerifyStatus.setText(R.string.linked_verifying);
+ displayButton(on ? 2 : 0);
+ } else if (success) {
+ mVerifyStatus.setText(R.string.linked_verify_success);
+ mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
+ PorterDuff.Mode.SRC_IN);
+ displayButton(2);
+ } else {
+ mVerifyStatus.setText(R.string.linked_verify_error);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
+ PorterDuff.Mode.SRC_IN);
+ displayButton(1);
+ }
+ mVerifyAnimator.setDisplayedChild(on ? 1 : 0);
+ }
+
+ public void displayButton(int button) {
+ if (mVerifyButtonAnimator.getDisplayedChild() == button) {
+ return;
+ }
+ mVerifyButtonAnimator.setDisplayedChild(button);
+ }
+
+ protected void proofVerify() {
+ setVerifyProgress(true, null);
+
+ new AsyncTask<Void,Void,LinkedVerifyResult>() {
+
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ long timer = System.currentTimeMillis();
+
+ LinkedCookieResource resource = getResource();
+ LinkedVerifyResult result = resource.verify(mLinkedIdWizard.mFingerprint);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ if (result.success()) {
+ mVerifiedResource = resource;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ super.onPostExecute(result);
+ if (result.success()) {
+ setVerifyProgress(false, true);
+ } else {
+ setVerifyProgress(false, false);
+ // on error, show error message
+ result.createNotify(getActivity()).show();
+ }
+ }
+ }.execute();
+
+ }
+
+ private void startCertify() {
+
+ if (mVerifiedResource == null) {
+ Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR).show();
+ return;
+ }
+
+ Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mLinkedIdWizard.mMasterKeyId);
+ startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
+
+ }
+
+ private void certifyLinkedIdentity (String passphrase) {
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
+ getActivity(),
+ getString(R.string.progress_saving),
+ ProgressDialog.STYLE_HORIZONTAL,
+ true) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == MessageStatus.OKAY.ordinal()) {
+
+ // get returned data bundle
+ Bundle returnData = message.getData();
+ if (returnData == null) {
+ return;
+ }
+ final OperationResult result =
+ returnData.getParcelable(OperationResult.EXTRA_RESULT);
+ if (result == null) {
+ return;
+ }
+
+ // if bad -> display here!
+ if (!result.success()) {
+ result.createNotify(getActivity()).show();
+ return;
+ }
+
+ getActivity().finish();
+
+ }
+ }
+ };
+
+ SaveKeyringParcel skp =
+ new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
+
+ WrappedUserAttribute ua =
+ LinkedIdentity.fromResource(mVerifiedResource).toUserAttribute();
+
+ skp.mAddUserAttribute.add(ua);
+
+ // Send all information needed to service to import key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+ intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.EDIT_KEYRING_PASSPHRASE, passphrase);
+ data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, skp);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PASSPHRASE:
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String passphrase =
+ data.getStringExtra(PassphraseDialogActivity.MESSAGE_DATA_PASSPHRASE);
+ certifyLinkedIdentity(passphrase);
+ }
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep1Fragment.java
new file mode 100644
index 000000000..77eccf3be
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep1Fragment.java
@@ -0,0 +1,129 @@
+/*
+ * 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.linked;
+
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+
+public class LinkedIdCreateGithubStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditHandle;
+
+ public static LinkedIdCreateGithubStep1Fragment newInstance() {
+ LinkedIdCreateGithubStep1Fragment frag = new LinkedIdCreateGithubStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_github_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String handle = mEditHandle.getText().toString();
+
+ new AsyncTask<Void,Void,Boolean>() {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return true; // return checkHandle(handle);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result == null) {
+ Notify.create(getActivity(),
+ "Connection error while checking username!", Notify.Style.ERROR).show();
+ return;
+ }
+
+ if (!result) {
+ Notify.create(getActivity(),
+ "This handle does not exist on Github!", Notify.Style.ERROR).show();
+ return;
+ }
+
+ LinkedIdCreateGithubStep2Fragment frag =
+ LinkedIdCreateGithubStep2Fragment.newInstance(handle);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ }.execute();
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditHandle = (EditText) view.findViewById(R.id.linked_create_github_handle);
+ mEditHandle.setText("Valodim");
+
+ return view;
+ }
+
+ private static Boolean checkHandle(String handle) {
+ try {
+ HttpURLConnection nection =
+ (HttpURLConnection) new URL("https://api.github.com/" + handle).openConnection();
+ nection.setRequestMethod("HEAD");
+ return nection.getResponseCode() == 200;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep2Fragment.java
new file mode 100644
index 000000000..05bdf89a4
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubStep2Fragment.java
@@ -0,0 +1,111 @@
+/*
+ * 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.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GithubResource;
+
+
+public class LinkedIdCreateGithubStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ public static final String ARG_HANDLE = "handle";
+
+ String mResourceHandle;
+ String mResourceString;
+
+ public static LinkedIdCreateGithubStep2Fragment newInstance
+ (String handle) {
+
+ LinkedIdCreateGithubStep2Fragment frag = new LinkedIdCreateGithubStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_HANDLE, handle);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mResourceString =
+ GithubResource.generate(getActivity(), mLinkedIdWizard.mFingerprint);
+
+ mResourceHandle = getArguments().getString(ARG_HANDLE);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofShare();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ LinkedCookieResource getResource() {
+ return GithubResource.searchInGithubStream(mResourceHandle, mResourceString);
+ }
+
+ @Override
+ protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_github_fragment_step2, container, false);
+ }
+
+ private void proofShare() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSend() {
+ Uri.Builder builder = Uri.parse("https://gist.github.com/").buildUpon();
+ builder.appendQueryParameter("text", mResourceString);
+ Uri uri = builder.build();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ getActivity().startActivity(intent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
new file mode 100644
index 000000000..72669142b
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+
+public class LinkedIdCreateHttpsStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditUri;
+
+ public static LinkedIdCreateHttpsStep1Fragment newInstance() {
+ LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String uri = "https://" + mEditUri.getText();
+
+ if (!checkUri(uri)) {
+ return;
+ }
+
+ String proofText = GenericHttpsResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint);
+
+ LinkedIdCreateHttpsStep2Fragment frag =
+ LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+
+ mEditUri.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String uri = "https://" + editable;
+ if (uri.length() > 0) {
+ if (checkUri(uri)) {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_ok, 0);
+ } else {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_bad, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ });
+
+ mEditUri.setText("mugenguild.com/pgpkey.txt");
+
+ return view;
+ }
+
+ private static boolean checkUri(String uri) {
+ return Patterns.WEB_URL.matcher(uri).matches();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
new file mode 100644
index 000000000..5559c0daf
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
@@ -0,0 +1,172 @@
+/*
+ * 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.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+
+ public static final String ARG_URI = "uri", ARG_TEXT = "text";
+
+ EditText mEditUri;
+
+ URI mResourceUri;
+ String mResourceString;
+
+ public static LinkedIdCreateHttpsStep2Fragment newInstance
+ (String uri, String proofText) {
+
+ LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_URI, uri);
+ args.putString(ARG_TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ GenericHttpsResource getResource() {
+ return GenericHttpsResource.createNew(mResourceUri);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ mResourceUri = new URI(getArguments().getString(ARG_URI));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ getActivity().finish();
+ }
+
+ mResourceString = getArguments().getString(ARG_TEXT);
+
+ }
+
+ protected View newView(LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSave();
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+ mEditUri.setText(mResourceUri.toString());
+
+ return view;
+ }
+
+ private void proofSend () {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSave () {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ Notify.create(getActivity(), "External storage not available!", Style.ERROR);
+ return;
+ }
+
+ String targetName = "pgpkey.txt";
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ File targetFile = new File(Constants.Path.APP_DIR, targetName);
+ 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, "text/plain", targetName, REQUEST_CODE_OUTPUT);
+ }
+ }
+
+ private void saveFile(Uri uri) {
+ try {
+ PrintWriter out =
+ new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
+ out.print(mResourceString);
+ if (out.checkError()) {
+ Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
+ }
+ } catch (FileNotFoundException e) {
+ Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // For saving a file
+ case REQUEST_CODE_OUTPUT:
+ if (data == null) {
+ return;
+ }
+ Uri uri = data.getData();
+ saveFile(uri);
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
new file mode 100644
index 000000000..c36f98058
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
@@ -0,0 +1,127 @@
+/*
+ * 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.linked;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditHandle;
+
+ public static LinkedIdCreateTwitterStep1Fragment newInstance() {
+ LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String handle = mEditHandle.getText().toString();
+
+ new AsyncTask<Void,Void,Boolean>() {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return true; // return checkHandle(handle);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result == null) {
+ Notify.create(getActivity(),
+ "Connection error while checking username!", Notify.Style.ERROR);
+ return;
+ }
+
+ if (!result) {
+ Notify.create(getActivity(),
+ "This handle does not exist on Twitter!", Notify.Style.ERROR);
+ return;
+ }
+
+ LinkedIdCreateTwitterStep2Fragment frag =
+ LinkedIdCreateTwitterStep2Fragment.newInstance(handle);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ }.execute();
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle);
+ mEditHandle.setText("v_debug");
+
+ return view;
+ }
+
+ private static Boolean checkHandle(String handle) {
+ try {
+ HttpURLConnection nection =
+ (HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
+ nection.setRequestMethod("HEAD");
+ return nection.getResponseCode() == 200;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
new file mode 100644
index 000000000..2de9b54c9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
@@ -0,0 +1,112 @@
+/*
+ * 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.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.TwitterResource;
+
+public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ public static final String ARG_HANDLE = "handle";
+
+ String mResourceHandle;
+ String mResourceString;
+
+ public static LinkedIdCreateTwitterStep2Fragment newInstance
+ (String handle) {
+
+ LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_HANDLE, handle);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mResourceString =
+ TwitterResource.generate(getActivity(), mLinkedIdWizard.mFingerprint);
+
+ mResourceHandle = getArguments().getString(ARG_HANDLE);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofShare();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ LinkedCookieResource getResource() {
+ return TwitterResource.searchInTwitterStream(mResourceHandle, mResourceString);
+ }
+
+ @Override
+ protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false);
+ }
+
+ private void proofShare() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSend() {
+
+ Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon();
+ builder.appendQueryParameter("text", mResourceString);
+ Uri uri = builder.build();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ getActivity().startActivity(intent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
new file mode 100644
index 000000000..8249f0bb6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
@@ -0,0 +1,103 @@
+/*
+ * 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.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.sufficientlysecure.keychain.R;
+
+public class LinkedIdSelectFragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdSelectFragment newInstance() {
+ LinkedIdSelectFragment frag = new LinkedIdSelectFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.linked_select_fragment, container, false);
+
+ view.findViewById(R.id.linked_create_https_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateHttpsStep1Fragment frag =
+ LinkedIdCreateHttpsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_dns_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateDnsStep1Fragment frag =
+ LinkedIdCreateDnsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_twitter_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateTwitterStep1Fragment frag =
+ LinkedIdCreateTwitterStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_github_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateGithubStep1Fragment frag =
+ LinkedIdCreateGithubStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
new file mode 100644
index 000000000..41d0178a3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
@@ -0,0 +1,594 @@
+package org.sufficientlysecure.keychain.ui.linked;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.CertifyResult;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedResource;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
+import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.ViewHolder.VerifyState;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.ui.widget.CertListWidget;
+import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+
+public class LinkedIdViewFragment extends Fragment implements
+ LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener {
+
+ public static final int REQUEST_CODE_PASSPHRASE = 0x00008001;
+
+ private static final String ARG_DATA_URI = "data_uri";
+ private static final String ARG_LID_RANK = "rank";
+ private static final String ARG_IS_SECRET = "verified";
+ private static final String ARG_FINGERPRINT = "fingerprint";
+ private static final int LOADER_ID_LINKED_ID = 1;
+
+ private RawLinkedIdentity mLinkedId;
+ private LinkedCookieResource mLinkedResource;
+ private boolean mIsSecret;
+
+ private Context mContext;
+ private byte[] mFingerprint;
+
+ private AsyncTask mInProgress;
+
+ private Uri mDataUri;
+ private ViewHolder mViewHolder;
+ private int mLidRank;
+ private OnIdentityLoadedListener mIdLoadedListener;
+
+ public static LinkedIdViewFragment newInstance(Uri dataUri, int rank,
+ boolean isSecret, byte[] fingerprint) throws IOException {
+ LinkedIdViewFragment frag = new LinkedIdViewFragment();
+
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putInt(ARG_LID_RANK, rank);
+ args.putBoolean(ARG_IS_SECRET, isSecret);
+ args.putByteArray(ARG_FINGERPRINT, fingerprint);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Bundle args = getArguments();
+ mDataUri = args.getParcelable(ARG_DATA_URI);
+ mLidRank = args.getInt(ARG_LID_RANK);
+
+ mIsSecret = args.getBoolean(ARG_IS_SECRET);
+ mFingerprint = args.getByteArray(ARG_FINGERPRINT);
+
+ mContext = getActivity();
+
+ getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this);
+
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_LINKED_ID:
+ return new CursorLoader(getActivity(), mDataUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION,
+ Tables.USER_PACKETS + "." + UserPackets.RANK
+ + " = " + Integer.toString(mLidRank), null, null);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ switch (loader.getId()) {
+ case LOADER_ID_LINKED_ID:
+
+ if (!cursor.moveToFirst()) {
+ Notify.createNotify(getActivity(), "Error loading identity!",
+ Notify.LENGTH_LONG, Style.ERROR).show();
+ finishFragment();
+ break;
+ }
+
+ try {
+ int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED);
+
+ byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA);
+ RawLinkedIdentity linkedId = LinkedIdentity.fromAttributeData(data);
+
+ loadIdentity(linkedId, certStatus);
+
+ if (mIdLoadedListener != null) {
+ mIdLoadedListener.onIdentityLoaded();
+ mIdLoadedListener = null;
+ }
+
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "error parsing identity", e);
+ Notify.create(getActivity(), "Error parsing identity!",
+ Notify.LENGTH_LONG, Style.ERROR).show();
+ finishFragment();
+ }
+
+ break;
+ }
+ }
+
+ public void finishFragment() {
+ FragmentManager manager = getFragmentManager();
+ manager.removeOnBackStackChangedListener(this);
+ manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+
+ public interface OnIdentityLoadedListener {
+ public void onIdentityLoaded();
+ }
+
+ public void setOnIdentityLoadedListener(OnIdentityLoadedListener listener) {
+ mIdLoadedListener = listener;
+ }
+
+ private void loadIdentity(RawLinkedIdentity linkedId, int certStatus) {
+ mLinkedId = linkedId;
+
+ setShowVerifying(false);
+
+ if (mLinkedId instanceof LinkedIdentity) {
+ LinkedResource res = ((LinkedIdentity) mLinkedId).mResource;
+ mLinkedResource = (LinkedCookieResource) res;
+ }
+
+ if (!mIsSecret) {
+ mViewHolder.mLinkedIdHolder.vVerified.setVisibility(View.VISIBLE);
+
+ switch (certStatus) {
+ case Certs.VERIFIED_SECRET:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ case Certs.VERIFIED_SELF:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ default:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ }
+ } else {
+ mViewHolder.mLinkedIdHolder.vVerified.setVisibility(View.GONE);
+ }
+
+ mViewHolder.mLinkedIdHolder.setData(mContext, mLinkedId);
+
+ // no resource, nothing further we can do…
+ if (mLinkedResource == null) {
+ mViewHolder.vButtonView.setVisibility(View.GONE);
+ mViewHolder.vButtonVerify.setVisibility(View.GONE);
+ return;
+ }
+
+ if (mLinkedResource.isViewable()) {
+ mViewHolder.vButtonView.setVisibility(View.VISIBLE);
+ mViewHolder.vButtonView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = mLinkedResource.getViewIntent();
+ if (intent == null) {
+ return;
+ }
+ getActivity().startActivity(intent);
+ }
+ });
+ } else {
+ mViewHolder.vButtonView.setVisibility(View.GONE);
+ }
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+
+ }
+
+ static class ViewHolder {
+ private final View vButtonView;
+ private final ViewAnimator vVerifyingContainer;
+ private final ViewAnimator vItemCertified;
+ LinkedIdsAdapter.ViewHolder mLinkedIdHolder;
+
+ private ViewAnimator vButtonSwitcher;
+ private CertListWidget vLinkedCerts;
+ private CertifyKeySpinner vKeySpinner;
+ private final View vButtonVerify;
+ private final View vButtonRetry;
+ private final View vButtonConfirm;
+ private final View vButtonBack;
+
+ private final ViewAnimator vProgress;
+ private final TextView vText;
+
+ ViewHolder(View root) {
+ vLinkedCerts = (CertListWidget) root.findViewById(R.id.linked_id_certs);
+ vKeySpinner = (CertifyKeySpinner) root.findViewById(R.id.cert_key_spinner);
+ vButtonSwitcher = (ViewAnimator) root.findViewById(R.id.button_animator);
+
+ mLinkedIdHolder = new LinkedIdsAdapter.ViewHolder(root);
+
+ vButtonBack = root.findViewById(R.id.back_button);
+ vButtonVerify = root.findViewById(R.id.button_verify);
+ vButtonRetry = root.findViewById(R.id.button_retry);
+ vButtonConfirm = root.findViewById(R.id.button_confirm);
+ vButtonView = root.findViewById(R.id.button_view);
+
+ vVerifyingContainer = (ViewAnimator) root.findViewById(R.id.linked_verify_container);
+ vItemCertified = (ViewAnimator) root.findViewById(R.id.linked_id_certified);
+
+ vProgress = (ViewAnimator) root.findViewById(R.id.linked_cert_progress);
+ vText = (TextView) root.findViewById(R.id.linked_cert_text);
+ }
+
+ enum VerifyState {
+ VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING
+ }
+
+ void setVerifyingState(VerifyState state, boolean isSecret) {
+ switch (state) {
+ case VERIFYING:
+ vProgress.setDisplayedChild(0);
+ vText.setText("Verifying…");
+ vKeySpinner.setVisibility(View.GONE);
+ break;
+
+ case VERIFY_OK:
+ vProgress.setDisplayedChild(1);
+ if (!isSecret) {
+ showButton(2);
+ vKeySpinner.setVisibility(View.VISIBLE);
+ } else {
+ showButton(1);
+ vKeySpinner.setVisibility(View.GONE);
+ }
+ break;
+
+ case VERIFY_ERROR:
+ showButton(1);
+ vProgress.setDisplayedChild(2);
+ vText.setText("Error");
+ vKeySpinner.setVisibility(View.GONE);
+ break;
+
+ case CERTIFYING:
+ vProgress.setDisplayedChild(0);
+ vText.setText("Confirming…");
+ vKeySpinner.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ void showVerifyingContainer(boolean show) {
+ if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) {
+ return;
+ }
+
+ vVerifyingContainer.setDisplayedChild(show ? 1 : 0);
+ vItemCertified.setDisplayedChild(show ? 1 : 0);
+ }
+
+ void showButton(int which) {
+ if (vButtonSwitcher.getDisplayedChild() == which) {
+ return;
+ }
+ vButtonSwitcher.setDisplayedChild(which);
+ }
+
+ }
+
+ private boolean mVerificationState = false;
+ /** Switches between the 'verifying' ui bit and certificate status. This method
+ * must behave correctly in all states, showing or hiding the appropriate views
+ * and cancelling pending operations where necessary.
+ *
+ * This method also handles back button functionality in combination with
+ * onBackStateChanged.
+ */
+ void setShowVerifying(boolean show) {
+ if (!show) {
+ if (mInProgress != null) {
+ mInProgress.cancel(false);
+ mInProgress = null;
+ }
+ getFragmentManager().removeOnBackStackChangedListener(this);
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().popBackStack("verification",
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ });
+
+ if (!mVerificationState) {
+ return;
+ }
+ mVerificationState = false;
+
+ mViewHolder.showButton(0);
+ mViewHolder.vKeySpinner.setVisibility(View.GONE);
+ mViewHolder.showVerifyingContainer(false);
+ return;
+ }
+
+ if (mVerificationState) {
+ return;
+ }
+ mVerificationState = true;
+
+ FragmentManager manager = getFragmentManager();
+ manager.beginTransaction().addToBackStack("verification").commit();
+ manager.executePendingTransactions();
+ manager.addOnBackStackChangedListener(this);
+ mViewHolder.showVerifyingContainer(true);
+
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ setShowVerifying(false);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.linked_id_view_fragment, null);
+
+ mViewHolder = new ViewHolder(root);
+ root.setTag(mViewHolder);
+
+ ((ImageView) root.findViewById(R.id.status_icon_verified))
+ .setColorFilter(mContext.getResources().getColor(R.color.android_green_light),
+ PorterDuff.Mode.SRC_IN);
+ ((ImageView) root.findViewById(R.id.status_icon_invalid))
+ .setColorFilter(mContext.getResources().getColor(R.color.android_red_light),
+ PorterDuff.Mode.SRC_IN);
+
+
+
+ mViewHolder.vButtonBack.setClickable(true);
+ mViewHolder.vButtonBack.findViewById(R.id.back_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getFragmentManager().popBackStack();
+ }
+ });
+
+ mViewHolder.vButtonVerify.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ verifyResource();
+ }
+ });
+ mViewHolder.vButtonRetry.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ verifyResource();
+ }
+ });
+ mViewHolder.vButtonConfirm.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ initiateCertifying();
+ }
+ });
+
+ {
+ Bundle args = new Bundle();
+ args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(mDataUri, mLidRank));
+ args.putBoolean(CertListWidget.ARG_IS_SECRET, mIsSecret);
+ getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS,
+ args, mViewHolder.vLinkedCerts);
+ }
+
+ return root;
+ }
+
+ void verifyResource() {
+
+ // only one at a time (no sync needed, mInProgress is only touched in ui thread)
+ if (mInProgress != null) {
+ return;
+ }
+
+ setShowVerifying(true);
+
+ mViewHolder.vKeySpinner.setVisibility(View.GONE);
+ mViewHolder.setVerifyingState(VerifyState.VERIFYING, mIsSecret);
+
+ mInProgress = new AsyncTask<Void,Void,LinkedVerifyResult>() {
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ long timer = System.currentTimeMillis();
+ LinkedVerifyResult result = mLinkedResource.verify(mFingerprint);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ if (isCancelled()) {
+ return;
+ }
+ if (result.success()) {
+ mViewHolder.vText.setText(mLinkedResource.getVerifiedText());
+ mViewHolder.setVerifyingState(VerifyState.VERIFY_OK, mIsSecret);
+ } else {
+ mViewHolder.setVerifyingState(VerifyState.VERIFY_ERROR, mIsSecret);
+ result.createNotify(getActivity()).show();
+ }
+ mInProgress = null;
+ }
+ }.execute();
+
+ }
+
+ private void initiateCertifying() {
+
+ if (mIsSecret) {
+ return;
+ }
+
+ // get the user's passphrase for this key (if required)
+ Passphrase passphrase;
+ long certifyKeyId = mViewHolder.vKeySpinner.getSelectedItemId();
+ try {
+ passphrase = PassphraseCacheService.getCachedPassphrase(
+ getActivity(), certifyKeyId, certifyKeyId);
+ } catch (PassphraseCacheService.KeyNotFoundException e) {
+ Log.e(Constants.TAG, "Key not found!", e);
+ getActivity().finish();
+ return;
+ }
+ if (passphrase == null) {
+ Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, certifyKeyId);
+ startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
+ // bail out; need to wait until the user has entered the passphrase before trying again
+ } else {
+ certifyResource(certifyKeyId, "");
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PASSPHRASE: {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String passphrase = data.getStringExtra(
+ PassphraseDialogActivity.MESSAGE_DATA_PASSPHRASE);
+ long certifyKeyId = data.getLongExtra(PassphraseDialogActivity.EXTRA_KEY_ID, 0L);
+ if (certifyKeyId == 0L) {
+ throw new AssertionError("key id must not be 0");
+ }
+ certifyResource(certifyKeyId, passphrase);
+ }
+ return;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ private void certifyResource(long certifyKeyId, String passphrase) {
+
+ if (mIsSecret) {
+ return;
+ }
+
+ mViewHolder.setVerifyingState(VerifyState.CERTIFYING, false);
+
+ Bundle data = new Bundle();
+ {
+
+ long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint);
+ CertifyAction action = new CertifyAction(masterKeyId, null,
+ Arrays.asList(mLinkedId.toUserAttribute()));
+
+ // fill values for this action
+ CertifyActionsParcel parcel = new CertifyActionsParcel(certifyKeyId);
+ parcel.mCertifyActions.addAll(Arrays.asList(action));
+
+ data.putParcelable(KeychainIntentService.CERTIFY_PARCEL, parcel);
+ /* if (mUploadKeyCheckbox.isChecked()) {
+ String keyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, keyserver);
+ } */
+ }
+
+ // Send all information needed to service to sign key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+ intent.setAction(KeychainIntentService.ACTION_CERTIFY_KEYRING);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after signing is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity()) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ Bundle data = message.getData();
+
+ if (message.arg1 == MessageStatus.OKAY.ordinal()) {
+ CertifyResult result = data.getParcelable(CertifyResult.EXTRA_RESULT);
+ result.createNotify(getActivity()).show();
+ }
+
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // start service with intent
+ getActivity().startService(intent);
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
new file mode 100644
index 000000000..161efc8fb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
@@ -0,0 +1,106 @@
+/*
+ * 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.linked;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.ActionBarActivity;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class LinkedIdWizard extends ActionBarActivity {
+
+ public static final int FRAG_ACTION_START = 0;
+ public static final int FRAG_ACTION_TO_RIGHT = 1;
+ public static final int FRAG_ACTION_TO_LEFT = 2;
+
+ long mMasterKeyId;
+ byte[] mFingerprint;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.create_key_activity);
+
+ try {
+ Uri uri = getIntent().getData();
+ uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri);
+ CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri);
+ if (!ring.hasAnySecret()) {
+ Log.e(Constants.TAG, "Linked Identities can only be added to secret keys!");
+ finish();
+ return;
+ }
+
+ mMasterKeyId = ring.extractOrGetMasterKeyId();
+ mFingerprint = ring.getFingerprint();
+ } catch (PgpKeyNotFoundException e) {
+ Log.e(Constants.TAG, "Invalid uri given, key does not exist!");
+ finish();
+ return;
+ }
+
+ // pass extras into fragment
+ LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance();
+ loadFragment(null, frag, FRAG_ACTION_START);
+ }
+
+ public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) {
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+
+ switch (action) {
+ case FRAG_ACTION_START:
+ transaction.setCustomAnimations(0, 0);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+ case FRAG_ACTION_TO_LEFT:
+ getSupportFragmentManager().popBackStackImmediate();
+ break;
+ case FRAG_ACTION_TO_RIGHT:
+ transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left,
+ R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right);
+ transaction.addToBackStack(null);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+
+ }
+ // do it immediately!
+ getSupportFragmentManager().executePendingTransactions();
+ }
+
+}
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 c5403e054..57a71b084 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
@@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.util.Log;
+import java.nio.ByteBuffer;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -249,6 +250,10 @@ public class KeyFormattingUtils {
return hexString;
}
+ public static long convertFingerprintToKeyId(byte[] fingerprint) {
+ return ByteBuffer.wrap(fingerprint, 12, 8).getLong();
+ }
+
/**
* Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no
* leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4)
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java
new file mode 100644
index 000000000..c8f80f4a5
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java
@@ -0,0 +1,145 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+import java.util.Date;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import com.ocpsoft.pretty.time.PrettyTime;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+
+public class CertListWidget extends ViewAnimator
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ public static final int LOADER_ID_LINKED_CERTS = 38572;
+
+ public static final String ARG_URI = "uri";
+ public static final String ARG_IS_SECRET = "is_secret";
+
+
+ // These are the rows that we will retrieve.
+ static final String[] CERTS_PROJECTION = new String[]{
+ KeychainContract.Certs._ID,
+ KeychainContract.Certs.MASTER_KEY_ID,
+ KeychainContract.Certs.VERIFIED,
+ KeychainContract.Certs.TYPE,
+ KeychainContract.Certs.RANK,
+ KeychainContract.Certs.KEY_ID_CERTIFIER,
+ KeychainContract.Certs.USER_ID,
+ KeychainContract.Certs.SIGNER_UID,
+ KeychainContract.Certs.CREATION
+ };
+ public static final int INDEX_MASTER_KEY_ID = 1;
+ public static final int INDEX_VERIFIED = 2;
+ public static final int INDEX_TYPE = 3;
+ public static final int INDEX_RANK = 4;
+ public static final int INDEX_KEY_ID_CERTIFIER = 5;
+ public static final int INDEX_USER_ID = 6;
+ public static final int INDEX_SIGNER_UID = 7;
+ public static final int INDEX_CREATION = 8;
+
+ private TextView vCollapsed;
+ private ListView vExpanded;
+ private View vExpandButton;
+ private boolean mIsSecret;
+
+ public CertListWidget(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ View root = getRootView();
+ vCollapsed = (TextView) root.findViewById(R.id.cert_collapsed_list);
+ vExpanded = (ListView) root.findViewById(R.id.cert_expanded_list);
+ vExpandButton = root.findViewById(R.id.cert_expand_button);
+
+ // for now
+ vExpandButton.setVisibility(View.GONE);
+ vExpandButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleExpanded();
+ }
+ });
+
+ // vExpanded.setAdapter(null);
+
+ }
+
+ void toggleExpanded() {
+ setDisplayedChild(getDisplayedChild() == 1 ? 0 : 1);
+ }
+
+ void setExpanded(boolean expanded) {
+ setDisplayedChild(expanded ? 1 : 0);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri uri = args.getParcelable(ARG_URI);
+ mIsSecret = args.getBoolean(ARG_IS_SECRET, false);
+ return new CursorLoader(getContext(), uri,
+ CERTS_PROJECTION, null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+
+ if (data == null || !data.moveToFirst()) {
+ return;
+ }
+
+ // TODO support external certificates
+ Date userCert = null;
+ while (!data.isAfterLast()) {
+
+ int verified = data.getInt(INDEX_VERIFIED);
+ Date creation = new Date(data.getLong(INDEX_CREATION) * 1000);
+
+ if (verified == Certs.VERIFIED_SECRET) {
+ if (userCert == null || userCert.after(creation)) {
+ userCert = creation;
+ }
+ }
+
+ data.moveToNext();
+ }
+
+ if (userCert != null) {
+ PrettyTime format = new PrettyTime();
+ if (mIsSecret) {
+ vCollapsed.setText("You created this identity "
+ + format.format(userCert) + ".");
+ } else {
+ vCollapsed.setText("You verified and confirmed this identity "
+ + format.format(userCert) + ".");
+ }
+ } else {
+ vCollapsed.setText("This identity is not yet verified or confirmed.");
+ }
+
+ setVisibility(View.VISIBLE);
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ setVisibility(View.GONE);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java
new file mode 100644
index 000000000..292343eb7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java
@@ -0,0 +1,38 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.graphics.*;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+/** */
+public class HttpsPrefixedText extends EditText {
+
+ private String mPrefix; // can be hardcoded for demo purposes
+ private Rect mPrefixRect = new Rect();
+
+ public HttpsPrefixedText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mPrefix = "https://";
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint());
+ }
+
+ @Override
+ public int getCompoundPaddingLeft() {
+ return super.getCompoundPaddingLeft() + mPrefixRect.width();
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
index 2c75c3a7d..70f4e7792 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
@@ -132,10 +132,6 @@ public abstract class KeySpinner extends TintSpinner implements LoaderManager.Lo
}
}
- public long getSelectedKeyId() {
- return mSelectedKeyId;
- }
-
public void setSelectedKeyId(long selectedKeyId) {
this.mSelectedKeyId = selectedKeyId;
}
@@ -293,6 +289,7 @@ public abstract class KeySpinner extends TintSpinner implements LoaderManager.Lo
}
return view;
}
+
}
boolean setStatus(Context context, Cursor cursor, ImageView statusView) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java
new file mode 100644
index 000000000..ab73f59b8
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java
@@ -0,0 +1,76 @@
+package org.sufficientlysecure.keychain.util;
+
+
+import android.database.Cursor;
+import android.database.CursorWrapper;
+
+public abstract class FilterCursorWrapper extends CursorWrapper {
+ private int[] mIndex;
+ private int mCount = 0;
+ private int mPos = 0;
+
+ public abstract boolean isVisible(Cursor cursor);
+
+ public FilterCursorWrapper(Cursor cursor) {
+ super(cursor);
+ mCount = super.getCount();
+ mIndex = new int[mCount];
+ for (int i = 0; i < mCount; i++) {
+ super.moveToPosition(i);
+ if (isVisible(cursor)) {
+ mIndex[mPos++] = i;
+ }
+ }
+ mCount = mPos;
+ mPos = 0;
+ super.moveToFirst();
+ }
+
+ @Override
+ public boolean move(int offset) {
+ return this.moveToPosition(mPos + offset);
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return this.moveToPosition(mPos + 1);
+ }
+
+ @Override
+ public boolean moveToPrevious() {
+ return this.moveToPosition(mPos - 1);
+ }
+
+ @Override
+ public boolean moveToFirst() {
+ return this.moveToPosition(0);
+ }
+
+ @Override
+ public boolean moveToLast() {
+ return this.moveToPosition(mCount - 1);
+ }
+
+ @Override
+ public boolean moveToPosition(int position) {
+ if (position >= mCount || position < 0) {
+ return false;
+ }
+ return super.moveToPosition(mIndex[position]);
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ public int getHiddenCount() {
+ return super.getCount() - mCount;
+ }
+
+ @Override
+ public int getPosition() {
+ return mPos;
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/anim/fade_in.xml b/OpenKeychain/src/main/res/anim/fade_in.xml
new file mode 100644
index 000000000..5e2b8be60
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_in.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:interpolator="@android:anim/bounce_interpolator"
+ android:duration="700"
+ />
+</set> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/anim/fade_in_up.xml b/OpenKeychain/src/main/res/anim/fade_in_up.xml
new file mode 100644
index 000000000..c190c0f53
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_in_up.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="10" android:toYDelta="0"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="200"
+ />
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:duration="200"
+ />
+</set> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/anim/fade_out.xml b/OpenKeychain/src/main/res/anim/fade_out.xml
new file mode 100644
index 000000000..f96bf3cb3
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_out.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:duration="300"
+ />
+</set> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/anim/fade_out_down.xml b/OpenKeychain/src/main/res/anim/fade_out_down.xml
new file mode 100644
index 000000000..523b180af
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_out_down.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="0.0" android:toYDelta="10"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="200"
+ />
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="150"
+ />
+</set> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_24dp.png
new file mode 100644
index 000000000..e9dfc47a4
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_96dp.png b/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_96dp.png
new file mode 100644
index 000000000..4a3f2b2b5
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-hdpi/status_signature_verified_inner_96dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_24dp.png
new file mode 100644
index 000000000..28e37d9f3
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_96dp.png b/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_96dp.png
new file mode 100644
index 000000000..cc7091df0
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-mdpi/status_signature_verified_inner_96dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_24dp.png
new file mode 100644
index 000000000..ea801c45a
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_96dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_96dp.png
new file mode 100644
index 000000000..61610df19
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xhdpi/status_signature_verified_inner_96dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_24dp.png
new file mode 100644
index 000000000..bcd73b09b
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_96dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_96dp.png
new file mode 100644
index 000000000..71e563218
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xxhdpi/status_signature_verified_inner_96dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/status_signature_verified_inner_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/status_signature_verified_inner_24dp.png
new file mode 100644
index 000000000..cc7091df0
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xxxhdpi/status_signature_verified_inner_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable/divider.xml b/OpenKeychain/src/main/res/drawable/divider.xml
new file mode 100644
index 000000000..b0695f89a
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/divider.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <size
+ android:height="1px"
+ android:width="1000dp" />
+
+ <solid android:color="@color/bg_gray" />
+
+</shape> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/drawable/dns.png b/OpenKeychain/src/main/res/drawable/dns.png
new file mode 100644
index 000000000..69d0a4fa8
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/dns.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable/github.png b/OpenKeychain/src/main/res/drawable/github.png
new file mode 100644
index 000000000..8b25551a9
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/github.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable/ssl_lock.png b/OpenKeychain/src/main/res/drawable/ssl_lock.png
new file mode 100644
index 000000000..00c4d8e4f
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/ssl_lock.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable/twitter.png b/OpenKeychain/src/main/res/drawable/twitter.png
new file mode 100644
index 000000000..3533e0488
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable/twitter.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/layout/cert_list_widget.xml b/OpenKeychain/src/main/res/layout/cert_list_widget.xml
new file mode 100644
index 000000000..715e4bfa5
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/cert_list_widget.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.sufficientlysecure.keychain.ui.widget.CertListWidget
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"
+ android:id="@+id/linked_id_certs">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ >
+
+ <TextView
+ android:id="@+id/cert_collapsed_list"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:layout_weight="1"
+ />
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_expand_more_black_24dp"
+ android:id="@+id/cert_expand_button"
+ android:padding="4dp"
+ />
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/cert_expanded_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ </ListView>
+
+</org.sufficientlysecure.keychain.ui.widget.CertListWidget> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step1.xml b/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step1.xml
new file mode 100644
index 000000000..c0a898843
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step1.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/dns"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_1_1" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_1_2" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_1_3" />
+
+ <EditText
+ android:id="@+id/linked_create_dns_domain"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:ems="10"
+ android:inputType="textUri"
+ android:layout_gravity="center_horizontal"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_1_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:clickable="true"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_next"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical|right"
+ android:clickable="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step2.xml b/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step2.xml
new file mode 100644
index 000000000..62e267f05
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_dns_fragment_step2.xml
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_2_1" />
+
+ <TextView
+ android:id="@+id/linked_create_dns_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_gravity="center_horizontal"
+ android:textIsSelectable="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_2_2" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle"
+ android:showDividers="middle">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@drawable/ic_content_copy_grey_24dp"
+ android:drawableStart="@android:drawable/ic_menu_share"
+ android:text="Copy"
+ android:id="@+id/button_save"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_share"
+ android:drawableStart="@android:drawable/ic_menu_share"
+ android:text="Share"
+ android:id="@+id/button_send"
+ />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_2_3" />
+
+ <include layout="@layout/linked_create_verify" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_dns_2_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_finish"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:drawableRight="@drawable/ic_person_add_grey_24dp"
+ android:drawablePadding="8dp"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:gravity="center_vertical|right"
+ android:clickable="true"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step1.xml b/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step1.xml
new file mode 100644
index 000000000..20c394afa
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step1.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_marginTop="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/certify_key_action_certify_image"
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/github"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_1_1" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_1_2" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_1_3" />
+
+ <EditText
+ android:id="@+id/linked_create_github_handle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:layout_marginTop="16dp"
+ android:ems="10"
+ android:layout_gravity="center_horizontal"
+ android:hint="@string/linked_create_github_handle"/>
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:clickable="true"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_next"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical|right"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step2.xml b/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step2.xml
new file mode 100644
index 000000000..81844e739
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_github_fragment_step2.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_2_1" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_2_2" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_send"
+ android:drawableStart="@android:drawable/ic_menu_send"
+ android:text="Post Gist"
+ android:id="@+id/button_send"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_share"
+ android:drawableStart="@android:drawable/ic_menu_share"
+ android:text="Share"
+ android:id="@+id/button_share"
+ />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_2_3" />
+
+ <include layout="@layout/linked_create_verify" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_github_2_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_finish"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ android:drawableRight="@drawable/ic_person_add_grey_24dp"
+ android:drawablePadding="8dp"
+ style="?android:attr/borderlessButtonStyle"
+ android:gravity="center_vertical|right"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step1.xml b/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step1.xml
new file mode 100644
index 000000000..5dfed35c4
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step1.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/certify_key_action_certify_image"
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/ssl_lock"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_1_1" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_1_2" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_1_3" />
+
+ <org.sufficientlysecure.keychain.ui.widget.HttpsPrefixedText
+ android:id="@+id/linked_create_https_uri"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:layout_marginTop="16dp"
+ android:ems="10"
+ android:inputType="textUri"
+ android:layout_gravity="center_horizontal"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_1_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:clickable="true"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_next"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical|right"
+ style="?android:attr/borderlessButtonStyle"
+ android:clickable="true"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step2.xml b/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step2.xml
new file mode 100644
index 000000000..dc6895a54
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_https_fragment_step2.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_2_1" />
+
+ <EditText
+ android:id="@+id/linked_create_https_uri"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:layout_marginTop="8dp"
+ android:hint="uri"
+ android:ems="10"
+ android:layout_gravity="center_horizontal"
+ android:inputType="textUri|none"
+ android:enabled="false"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_2_2" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle"
+ >
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_save"
+ android:drawableStart="@android:drawable/ic_menu_save"
+ android:text="Save"
+ android:id="@+id/button_save"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_share"
+ android:drawableStart="@android:drawable/ic_menu_share"
+ android:text="Share"
+ android:id="@+id/button_send"
+ />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_2_3" />
+
+ <include layout="@layout/linked_create_verify" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_https_2_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_finish"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ android:drawableRight="@drawable/ic_person_add_grey_24dp"
+ android:drawablePadding="8dp"
+ style="?android:attr/borderlessButtonStyle"
+ android:gravity="center_vertical|right"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step1.xml b/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step1.xml
new file mode 100644
index 000000000..76521fa11
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step1.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_marginTop="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/certify_key_action_certify_image"
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/twitter"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_1_1" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_1_2" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_1_3" />
+
+ <EditText
+ android:id="@+id/linked_create_twitter_handle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:layout_marginTop="16dp"
+ android:ems="10"
+ android:layout_gravity="center_horizontal"
+ android:hint="@string/linked_create_twitter_handle"/>
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:clickable="true"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_next"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical|right"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step2.xml b/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step2.xml
new file mode 100644
index 000000000..e42c23b51
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_twitter_fragment_step2.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="false"
+ android:layout_above="@+id/create_key_button_divider">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_2_1" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_2_2" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_send"
+ android:drawableStart="@android:drawable/ic_menu_send"
+ android:text="Tweet"
+ android:id="@+id/button_send"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="?android:buttonBarButtonStyle"
+ android:drawableLeft="@android:drawable/ic_menu_share"
+ android:drawableStart="@android:drawable/ic_menu_share"
+ android:text="Share"
+ android:id="@+id/button_share"
+ />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_2_3" />
+
+ <include layout="@layout/linked_create_verify" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_create_twitter_2_4" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <View
+ android:id="@+id/create_key_button_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/listDivider"
+ android:layout_alignTop="@+id/create_key_buttons"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:id="@+id/create_key_buttons">
+
+ <TextView
+ android:id="@+id/back_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_back"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ style="?android:attr/borderlessButtonStyle"
+ android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
+ android:drawablePadding="8dp"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/next_button"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_finish"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAllCaps="true"
+ android:drawableRight="@drawable/ic_person_add_grey_24dp"
+ android:drawablePadding="8dp"
+ style="?android:attr/borderlessButtonStyle"
+ android:gravity="center_vertical|right"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_create_verify.xml b/OpenKeychain/src/main/res/layout/linked_create_verify.xml
new file mode 100644
index 000000000..837bae09a
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_create_verify.xml
@@ -0,0 +1,79 @@
+<?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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ tools:showIn="@layout/linked_create_https_fragment_step2">
+
+ <ViewAnimator
+ android:id="@+id/verify_progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginStart="16dip"
+ android:layout_gravity="center_vertical"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ >
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:id="@+id/verify_image"
+ android:src="@drawable/status_signature_unverified_cutout_24dp"
+ />
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:indeterminateOnly="true" />
+
+ </ViewAnimator>
+
+ <TextView
+ android:id="@+id/verify_status"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_verify_pending"
+ android:layout_marginLeft="16dip"
+ android:layout_marginStart="16dip"
+ android:layout_weight="1"
+ />
+
+ <ViewAnimator
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="16dp"
+ android:layout_marginEnd="16dip"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:id="@+id/verify_buttons"
+ >
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:buttonBarButtonStyle"
+ android:text="Verify"
+ android:id="@+id/button_verify"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:buttonBarButtonStyle"
+ android:text="Retry"
+ android:id="@+id/button_retry"
+ />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </ViewAnimator>
+
+</LinearLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_id_item.xml b/OpenKeychain/src/main/res/layout/linked_id_item.xml
new file mode 100644
index 000000000..900d3a5d5
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_id_item.xml
@@ -0,0 +1,72 @@
+<?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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:singleLine="true"
+ tools:showIn="@layout/linked_id_view_fragment">
+
+ <ImageView
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:id="@+id/linked_id_type_icon"
+ android:layout_marginLeft="14dp"
+ android:layout_marginStart="14dp"
+ android:src="@drawable/dns"
+ android:layout_gravity="center_vertical"
+ android:scaleType="fitCenter" />
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical"
+ android:layout_width="0dip"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/linked_id_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Title"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/linked_id_comment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/tertiary_text_light"
+ android:text="comment"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ </LinearLayout>
+
+ <ViewAnimator
+ android:id="@+id/linked_id_certified"
+ android:layout_width="22dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:outAnimation="@anim/fade_out_down"
+ android:inAnimation="@anim/fade_in_up"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/linked_id_certified_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/status_signature_unknown_cutout_24dp"
+ android:layout_gravity="center_horizontal" />
+
+ <Space
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content" />
+
+ </ViewAnimator>
+
+</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml b/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml
new file mode 100644
index 000000000..2fb79a4bf
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp">
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/card_linked_ids"
+ android:transitionName="card_linked_ids"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ card_view:cardBackgroundColor="@android:color/white"
+ card_view:cardElevation="2dp"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardCornerRadius="4dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/CardViewHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Linked Identity" />
+
+ <include layout="@layout/linked_id_item" />
+
+ <ViewAnimator
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/linked_verify_container"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"
+ >
+
+ <include layout="@layout/cert_list_widget" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:singleLine="true">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical"
+ android:layout_width="0dip"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/linked_cert_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Verifying…"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+
+ <ViewAnimator
+ android:layout_width="22dp"
+ android:layout_height="22dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="4dp"
+ android:layout_gravity="center"
+ android:id="@+id/linked_cert_progress"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out">
+
+ <ProgressBar
+ android:layout_width="22dp"
+ android:layout_height="22dp"
+ android:indeterminate="true"
+ />
+
+ <ImageView
+ android:id="@+id/status_icon_verified"
+ android:layout_width="22dp"
+ android:layout_height="wrap_content"
+ android:src="@drawable/status_signature_verified_inner_24dp"
+ />
+
+ <ImageView
+ android:id="@+id/status_icon_invalid"
+ android:layout_width="22dp"
+ android:layout_height="wrap_content"
+ android:src="@drawable/status_signature_invalid_cutout_24dp"
+ />
+
+ </ViewAnimator>
+
+ </LinearLayout>
+
+ </ViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner
+ android:layout_marginLeft="14dp"
+ android:layout_marginRight="14dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/cert_key_spinner"
+ android:visibility="gone">
+ </org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/back_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_chevron_left_grey_24dp"
+ android:visibility="gone"
+ style="?android:attr/borderlessButtonStyle" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/button_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="View"
+ android:textColor="@color/link_text_material_light"
+ style="?android:attr/borderlessButtonStyle"
+ />
+
+ <ViewAnimator
+ android:id="@+id/button_animator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out">
+ <Button
+ android:id="@+id/button_verify"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Verify"
+ android:textColor="@color/link_text_material_light"
+ style="?android:attr/borderlessButtonStyle" />
+ <Button
+ android:id="@+id/button_retry"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Retry"
+ android:textColor="@color/link_text_material_light"
+ style="?android:attr/borderlessButtonStyle" />
+ <Button
+ android:id="@+id/button_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Confirm"
+ android:textColor="@color/link_text_material_light"
+ style="?android:attr/borderlessButtonStyle" />
+ </ViewAnimator>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</ScrollView> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/linked_select_fragment.xml b/OpenKeychain/src/main/res/layout/linked_select_fragment.xml
new file mode 100644
index 000000000..a183cdc93
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/linked_select_fragment.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_select_1"
+ android:id="@+id/textView"
+ android:layout_weight="1" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/linked_select_2"
+ android:layout_weight="1" />
+
+ <org.sufficientlysecure.keychain.ui.widget.FixedListView
+ android:id="@+id/view_key_user_ids"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="4dp"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:id="@+id/linked_create_https_button"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:paddingRight="4dp"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal">
+
+ <!-- separate ImageView required for recoloring -->
+ <ImageView
+ android:id="@+id/certify_key_action_certify_image"
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/ssl_lock"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:paddingLeft="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:text="Website (HTTPS)"
+ android:layout_weight="1"
+ android:gravity="center_vertical" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="4dp"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:id="@+id/linked_create_dns_button"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:paddingRight="4dp"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal">
+
+ <!-- separate ImageView required for recoloring -->
+ <ImageView
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/dns"
+ android:layout_gravity="center"
+ />
+
+ <TextView
+ android:paddingLeft="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:text="Domain Name (DNS)"
+ android:layout_weight="1"
+ android:gravity="center_vertical" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="4dp"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:id="@+id/linked_create_twitter_button"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:paddingRight="4dp"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal">
+
+ <!-- separate ImageView required for recoloring -->
+ <ImageView
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/twitter"
+ android:layout_gravity="center"
+ />
+
+ <TextView
+ android:paddingLeft="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:text="Twitter"
+ android:layout_weight="1"
+ android:gravity="center_vertical" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="4dp"
+ android:background="?android:attr/listDivider" />
+
+ <LinearLayout
+ android:id="@+id/linked_create_github_button"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:clickable="true"
+ android:paddingRight="4dp"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal">
+
+ <!-- separate ImageView required for recoloring -->
+ <ImageView
+ android:layout_width="60dip"
+ android:layout_height="60dip"
+ android:padding="8dp"
+ android:src="@drawable/github"
+ android:layout_gravity="center"
+ />
+
+ <TextView
+ android:paddingLeft="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:text="Github"
+ android:layout_weight="1"
+ android:gravity="center_vertical" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="4dp"
+ android:background="?android:attr/listDivider" />
+
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/view_key_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_fragment.xml
index 454d5f9c7..f842f764d 100644
--- a/OpenKeychain/src/main/res/layout/view_key_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/view_key_fragment.xml
@@ -13,7 +13,7 @@
android:paddingRight="16dp">
<android.support.v7.widget.CardView
- android:id="@+id/card_view"
+ android:id="@+id/card_identities"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -39,6 +39,58 @@
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"/>
</LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/card_linked_ids"
+ android:transitionName="card_linked_ids"
+ android:layout_gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ card_view:cardBackgroundColor="@android:color/white"
+ card_view:cardElevation="2dp"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardCornerRadius="4dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:animateLayoutChanges="true">
+
+ <TextView
+ style="@style/CardViewHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/section_linked_identities" />
+
+ <org.sufficientlysecure.keychain.ui.widget.FixedListView
+ android:id="@+id/view_key_linked_ids"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp" />
+
+ <TextView
+ android:id="@+id/view_key_linked_ids_expander"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:gravity="center_vertical"
+ android:drawableTop="@drawable/divider"
+ android:drawableRight="@drawable/ic_expand_more_black_24dp"
+ android:drawableEnd="@drawable/ic_expand_more_black_24dp"
+ android:drawablePadding="3dp"
+ android:clickable="true"
+ android:textColor="@color/tertiary_text_light"
+ android:text="%d more unknown identity types"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:background="?android:selectableItemBackground" />
+
+ </LinearLayout>
+
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView
diff --git a/OpenKeychain/src/main/res/menu/key_view.xml b/OpenKeychain/src/main/res/menu/key_view.xml
index 4a9fe16c7..3d1b02958 100644
--- a/OpenKeychain/src/main/res/menu/key_view.xml
+++ b/OpenKeychain/src/main/res/menu/key_view.xml
@@ -37,4 +37,9 @@
android:visible="false"
android:title="@string/menu_certify_fingerprint" />
+ <item
+ android:id="@+id/menu_key_view_add_linked_identity"
+ app:showAsAction="never"
+ android:title="@string/menu_linked_add_identity" />
+
</menu> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/transition/linked_id_card_trans.xml b/OpenKeychain/src/main/res/transition/linked_id_card_trans.xml
new file mode 100644
index 000000000..8d6e75cee
--- /dev/null
+++ b/OpenKeychain/src/main/res/transition/linked_id_card_trans.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
+ <changeBounds />
+</transitionSet> \ 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 f22a0c533..1ce8936b5 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -17,6 +17,7 @@
<string name="title_unlock">"Unlock Key"</string>
<string name="title_add_subkey">"Add subkey"</string>
<string name="title_edit_key">"Edit Key"</string>
+ <string name="title_linked_create">"Create a Linked Identity"</string>
<string name="title_preferences">"Settings"</string>
<string name="title_cloud_search_preferences">"Cloud Search Preferences"</string>
<string name="title_api_registered_apps">"Apps"</string>
@@ -423,11 +424,11 @@
<!-- Import result toast -->
<plurals name="import_keys_added_and_updated_1">
- <item quantity="one">"Successfully imported key"</item>
+ <item quantity="one">"Successfully imported one key"</item>
<item quantity="other">"Successfully imported %1$d keys"</item>
</plurals>
<plurals name="import_keys_added_and_updated_2">
- <item quantity="one">"and updated key%2$s."</item>
+ <item quantity="one">"and updated one key%2$s."</item>
<item quantity="other">"and updated %1$d keys%2$s."</item>
</plurals>
<plurals name="import_keys_added">
@@ -1167,6 +1168,21 @@
<item quantity="other">"Failed to delete %d keys"</item>
</plurals>
+ <!-- Linked identity verification -->
+ <string name="msg_lv">"Verifying linked identity…"</string>
+ <string name="msg_lv_match">"Searching for cookie"</string>
+ <string name="msg_lv_match_error">"No cookie found in resource!"</string>
+ <string name="msg_lv_fp_ok">"Fingerprint ok."</string>
+ <string name="msg_lv_fp_error">"Fingerprint mismatch!"</string>
+
+ <string name="msg_lv_fetch">"Fetching URI '%s'"</string>
+ <string name="msg_lv_fetch_redir">"Following redirect to '%s'"</string>
+ <string name="msg_lv_fetch_ok">"Successfully fetched (HTTP %s)"</string>
+ <string name="msg_lv_fetch_error">"Server error (HTTP %s)"</string>
+ <string name="msg_lv_fetch_error_url">"URL is malformed!"</string>
+ <string name="msg_lv_fetch_error_io">"IO Error!"</string>
+ <string name="msg_lv_fetch_error_format">"Format error!"</string>
+
<string name="msg_acc_saved">"Account saved"</string>
<string name="msg_download_success">"Downloaded successfully!"</string>
@@ -1264,4 +1280,66 @@
<string name="unlocked">Unlocked</string>
<string name="nfc_settings">Settings</string>
+ <string name="linked_create_https_1_1">"By creating a Linked Identity of this type, you can link your key to a website you control."</string>
+ <string name="linked_create_https_1_2">"To do this, you publish a text file on this website, then create a Linked Identity which links to it."</string>
+ <string name="linked_create_https_1_3">"Please enter a URL where you are able to place a text file for proof. Note that your server must support https and have a valid TLS certificate!"</string>
+ <string name="linked_create_https_1_4">"Example: https://example.com/pgpkey.txt"</string>
+ <string name="linked_create_https_created">"The proof file has been created. For the next step, you should save and upload it to the URI you indicated:"</string>
+ <string name="linked_create_https_2_1">"A proof file for this URI has been created:"</string>
+ <string name="linked_create_https_2_2">"For the next step, you should save and upload this file."</string>
+ <string name="linked_create_https_2_3">"Make sure the file is reachable at the correct URI, then verify your setup."</string>
+ <string name="linked_create_https_2_4">"After successful verification, press the Finish button to add the Linked Identity to your keyring and finish the process."</string>
+
+ <string name="linked_create_twitter_1_1">"By creating a Linked Identity of this type, you can link your key to a Twitter account you control."</string>
+ <string name="linked_create_twitter_1_2">"To do this, you publish a specific Tweet on your timeline, then create a Linked Identity which links to this Tweet."</string>
+ <string name="linked_create_twitter_1_3">"Please enter your Twitter screen name to proceed."</string>
+ <string name="linked_create_twitter_handle">Twitter Handle</string>
+ <string name="linked_create_twitter_2_1">"Click either button to tweet the message!"</string>
+ <string name="linked_create_twitter_2_2">"You can edit the Tweet before posting it, so long as the text inside the brackets is unmodified."</string>
+ <string name="linked_create_twitter_2_3">"Once your Tweet is published, click the Verify button to scan your timeline for it."</string>
+ <string name="linked_create_twitter_2_4">"After successful verification, press the Finish button to add the Linked Identity to your keyring and finish the process."</string>
+
+ <string name="linked_create_github_1_1">"By creating a Linked Identity of this type, you can link your key to a Github account you control."</string>
+ <string name="linked_create_github_1_2">"To do this, you publish a specific Gist on your timeline, then create a Linked Identity which links to this Gist."</string>
+ <string name="linked_create_github_1_3">"Please enter your Twitter screen name to proceed."</string>
+ <string name="linked_create_github_handle">Github Handle</string>
+ <string name="linked_create_github_2_1">"Click either button to post the gist!"</string>
+ <string name="linked_create_github_2_2">"You can edit the Gist before posting it, so long as the text inside the brackets is unmodified."</string>
+ <string name="linked_create_github_2_3">"Once your Gist is published, click the Verify button to scan your timeline for it."</string>
+ <string name="linked_create_github_2_4">"After successful verification, press the Finish button to add the Linked Identity to your keyring and finish the process."</string>
+
+ <string name="linked_create_dns_1_1">"By creating a Linked Identity of this type, you can link your key to a domain name you control."</string>
+ <string name="linked_create_dns_1_2">"To do this, you create a specific TXT record for the domain, then create a Linked Identity which links to this record."</string>
+ <!-- An Identity of this type is especially appropriate if your email address is at the same domain. -->
+ <string name="linked_create_dns_1_3">"Please enter a fully qualified domain name to proceed."</string>
+ <string name="linked_create_dns_1_4">"Example: subdomain.example.com"</string>
+ <string name="linked_create_dns_2_1">"Your proof text:"</string>
+ <string name="linked_create_dns_2_2">"For the next step, bla bla"</string>
+ <string name="linked_create_dns_2_3">"More bla:"</string>
+ <string name="linked_create_dns_2_4">"After successful verification, press the Finish button to add the Linked Identity to your keyring and finish the process."</string>
+
+ <string name="linked_create_verify">"Verify"</string>
+ <string name="linked_text_clipboard">Text has been copied to clipboard</string>
+ <string name="linked_verified_https">"The Website has been fetched and verified."</string>
+ <string name="linked_verified_github">"The Gist has been fetched and verified."</string>
+ <string name="linked_verified_dns">"The DNS record has been fetched and verified."</string>
+ <string name="linked_verified_twitter">"The Tweet has been fetched and verified."</string>
+ <plurals name="linked_id_expand">
+ <item quantity="one">"There is one more unknown identity type"</item>
+ <item quantity="other">"There are %d more unknown identity types"</item>
+ </plurals>
+
+ <string name="linked_select_1">"A \'linked identity\' connects your pgp key to a resource on the web."</string>
+ <string name="linked_select_2">"Please select a type:"</string>
+ <string name="linked_id_generic_text">"This file claims ownership of the OpenPGP key with long id %2$s.\n\nCookie for proof:\n%1$s"</string>
+ <string name="linked_id_github_text">"This Gist confirms the Linked Identity in my OpenPGP key, and links it to this Github account.\n\nCookie for proof:\n%1$s"</string>
+ <string name="linked_verifying">"Verifying…"</string>
+ <string name="linked_verify_success">"Verified!"</string>
+ <string name="linked_verify_error">"Verification error!"</string>
+ <string name="linked_verify_pending">"Not yet verified"</string>
+ <string name="linked_need_verify">The resource needs to be verified before you can proceed!</string>
+ <string name="menu_linked_add_identity">"Add Linked Identity"</string>
+ <string name="section_linked_identities">"Linked Identities"</string>
+ <string name="btn_finish">"Finish"</string>
+
</resources>
diff --git a/README.md b/README.md
index db61ab65a..d7e64f294 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+# Branch Description
+
+This is Valodim's topic branch for linked identities in OpenKeychain.
+Everything here is very much work-in-progress and subject to changes.
+Specification will follow.
+
# OpenKeychain (for Android)
OpenKeychain is an OpenPGP implementation for Android.
@@ -206,3 +212,6 @@ See https://github.com/open-keychain/open-keychain/blob/development/OpenKeychain
* Purple color scheme
http://android-holo-colors.com/
+
+* DNS icon
+ http://www.fatcow.com/free-icons