let users confirm each member in a conference even if that contact is already trusted

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java |  2 
src/main/java/eu/siacs/conversations/entities/Conversation.java         | 72 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java       |  4 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java          | 50 
src/main/res/layout/keys_card.xml                                       | 60 
src/main/res/values/strings.xml                                         |  1 
6 files changed, 137 insertions(+), 52 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -284,7 +284,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
 		HashSet<XmppAxolotlSession> sessions = new HashSet<>();
-		for(Jid jid : getCryptoTargets(conversation)) {
+		for(Jid jid : conversation.getAcceptedCryptoTargets()) {
 			sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
 		}
 		return sessions;

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -9,11 +9,13 @@ import net.java.otr4j.session.SessionID;
 import net.java.otr4j.session.SessionImpl;
 import net.java.otr4j.session.SessionStatus;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.security.interfaces.DSAPublicKey;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -48,6 +50,7 @@ public class Conversation extends AbstractEntity implements Blockable {
 	public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+	public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
 
 	private String name;
 	private String contactUuid;
@@ -313,6 +316,18 @@ public class Conversation extends AbstractEntity implements Blockable {
 		return getLongAttribute("last_clear_history", 0);
 	}
 
+	public List<Jid> getAcceptedCryptoTargets() {
+		if (mode == MODE_SINGLE) {
+			return Arrays.asList(getJid().toBareJid());
+		} else {
+			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
+		}
+	}
+
+	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
+		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
+	}
+
 	public void setCorrectingMessage(Message correctingMessage) {
 		this.correctingMessage = correctingMessage;
 	}
@@ -794,20 +809,59 @@ public class Conversation extends AbstractEntity implements Blockable {
 	}
 
 	public boolean setAttribute(String key, String value) {
-		try {
-			this.attributes.put(key, value);
-			return true;
-		} catch (JSONException e) {
-			return false;
+		synchronized (this.attributes) {
+			try {
+				this.attributes.put(key, value);
+				return true;
+			} catch (JSONException e) {
+				return false;
+			}
+		}
+	}
+
+	public boolean setAttribute(String key, List<Jid> jids) {
+		JSONArray array = new JSONArray();
+		for(Jid jid : jids) {
+			array.put(jid.toBareJid().toString());
+		}
+		synchronized (this.attributes) {
+			try {
+				this.attributes.put(key, array);
+				return true;
+			} catch (JSONException e) {
+				e.printStackTrace();
+				return false;
+			}
 		}
 	}
 
 	public String getAttribute(String key) {
-		try {
-			return this.attributes.getString(key);
-		} catch (JSONException e) {
-			return null;
+		synchronized (this.attributes) {
+			try {
+				return this.attributes.getString(key);
+			} catch (JSONException e) {
+				return null;
+			}
+		}
+	}
+
+	public List<Jid> getJidListAttribute(String key) {
+		ArrayList<Jid> list = new ArrayList<>();
+		synchronized (this.attributes) {
+			try {
+				JSONArray array = this.attributes.getJSONArray(key);
+				for (int i = 0; i < array.length(); ++i) {
+					try {
+						list.add(Jid.fromString(array.getString(i)));
+					} catch (InvalidJidException e) {
+						//ignored
+					}
+				}
+			} catch (JSONException e) {
+				//ignored
+			}
 		}
+		return list;
 	}
 
 	public int getIntAttribute(String key, int defaultValue) {

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -1531,11 +1531,12 @@ public class ConversationActivity extends XmppActivity
 	protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
 		AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
 		final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
+		boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
 		boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
 		boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
 		boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
 		boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
-		if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys) {
+		if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
 			axolotlService.createSessionsIfNeeded(mSelectedConversation);
 			Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
 			String[] contacts = new String[targets.size()];
@@ -1545,6 +1546,7 @@ public class ConversationActivity extends XmppActivity
 			intent.putExtra("contacts", contacts);
 			intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
 			intent.putExtra("choice", attachmentChoice);
+			intent.putExtra("conversation",mSelectedConversation.getUuid());
 			startActivityForResult(intent, requestCode);
 			return true;
 		} else {

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java 🔗

@@ -10,6 +10,8 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import android.util.Log;
+
 import org.whispersystems.libaxolotl.IdentityKey;
 
 import java.util.ArrayList;
@@ -18,19 +20,21 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
 public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
-	private Jid accountJid;
 	private List<Jid> contactJids;
 
 	private Account mAccount;
+	private Conversation mConversation;
 	private TextView keyErrorMessage;
 	private LinearLayout keyErrorMessageCard;
 	private TextView ownKeysTitle;
@@ -71,10 +75,6 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 	protected void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		setContentView(R.layout.activity_trust_keys);
-		try {
-			this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
-		} catch (final InvalidJidException ignored) {
-		}
 		this.contactJids = new ArrayList<>();
 		for(String jid : getIntent().getStringArrayExtra("contacts")) {
 			try {
@@ -126,13 +126,15 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 
 		synchronized (this.foreignKeysToTrust) {
 			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+				hasForeignKeys = true;
 				final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
+				final Jid jid = entry.getKey();
 				final TextView header = (TextView) layout.findViewById(R.id.foreign_keys_title);
 				final LinearLayout keysContainer = (LinearLayout) layout.findViewById(R.id.foreign_keys_details);
-				header.setText(entry.getKey().toString());
+				final TextView informNoKeys = (TextView) layout.findViewById(R.id.no_keys_to_accept);
+				header.setText(jid.toString());
 				final Map<String, Boolean> fingerprints = entry.getValue();
 				for (final String fingerprint : fingerprints.keySet()) {
-					hasForeignKeys = true;
 					addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
 							XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
 							new CompoundButton.OnCheckedChangeListener() {
@@ -146,11 +148,17 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 							null
 					);
 				}
+				if (fingerprints.size() == 0) {
+					informNoKeys.setVisibility(View.VISIBLE);
+					informNoKeys.setText(getString(R.string.no_keys_just_confirm,mAccount.getRoster().getContact(jid).getDisplayName()));
+				} else {
+					informNoKeys.setVisibility(View.GONE);
+				}
 				foreignKeys.addView(layout);
 			}
 		}
 
-		ownKeysTitle.setText(accountJid.toString());
+		ownKeysTitle.setText(mAccount.getJid().toBareJid().toString());
 		ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
 		foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
 		if(hasPendingKeyFetches()) {
@@ -176,6 +184,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 	}
 
 	private boolean reloadFingerprints() {
+		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
 		ownKeysToTrust.clear();
 		AxolotlService service = this.mAccount.getAxolotlService();
 		Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
@@ -197,7 +206,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 						foreignFingerprints.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
 					}
 				}
-				if (foreignFingerprints.size() > 0) {
+				if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) {
 					foreignKeysToTrust.put(jid, foreignFingerprints);
 				}
 			}
@@ -207,11 +216,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 
 	@Override
 	public void onBackendConnected() {
-		if (accountJid != null) {
-			this.mAccount = xmppConnectionService.findAccountByJid(accountJid);
-			if (this.mAccount == null) {
-				return;
-			}
+		Intent intent = getIntent();
+		this.mAccount = extractAccount(intent);
+		if (this.mAccount != null && intent != null) {
+			String uuid = intent.getStringExtra("conversation");
+			this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
 			reloadFingerprints();
 			populateView();
 		}
@@ -276,8 +285,14 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 					fingerprint,
 					XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
 		}
+		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
 		synchronized (this.foreignKeysToTrust) {
-			for (Map<String, Boolean> value : foreignKeysToTrust.values()) {
+			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+				Jid jid = entry.getKey();
+				Map<String, Boolean> value = entry.getValue();
+				if (!acceptedTargets.contains(jid)) {
+					acceptedTargets.add(jid);
+				}
 				for (final String fingerprint : value.keySet()) {
 					mAccount.getAxolotlService().setFingerprintTrust(
 							fingerprint,
@@ -285,6 +300,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 				}
 			}
 		}
+		if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
+			Log.d(Config.LOGTAG,"commiting accepted crypto targets: "+acceptedTargets);
+			mConversation.setAcceptedCryptoTargets(acceptedTargets);
+			//xmppConnectionService.updateConversation(mConversation);
+		}
 	}
 
 	private void unlock() {

src/main/res/layout/keys_card.xml 🔗

@@ -1,31 +1,39 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-android:id="@+id/foreign_keys_card"
-android:layout_width="fill_parent"
-android:layout_height="wrap_content"
-android:layout_marginLeft="@dimen/activity_horizontal_margin"
-android:layout_marginRight="@dimen/activity_horizontal_margin"
-android:layout_marginTop="@dimen/activity_vertical_margin"
-android:layout_marginBottom="@dimen/activity_vertical_margin"
-android:background="@drawable/infocard_border"
-android:orientation="vertical"
-android:padding="@dimen/infocard_padding">
+<LinearLayout android:id="@+id/foreign_keys_card"
+              xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="@dimen/activity_vertical_margin"
+              android:layout_marginLeft="@dimen/activity_horizontal_margin"
+              android:layout_marginRight="@dimen/activity_horizontal_margin"
+              android:layout_marginTop="@dimen/activity_vertical_margin"
+              android:background="@drawable/infocard_border"
+              android:orientation="vertical"
+              android:padding="@dimen/infocard_padding">
 
-<TextView
-    android:id="@+id/foreign_keys_title"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:textColor="@color/black87"
-    android:textSize="?attr/TextSizeHeadline"
-    android:textStyle="bold"/>
+    <TextView
+        android:id="@+id/foreign_keys_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@color/black87"
+        android:textSize="?attr/TextSizeHeadline"
+        android:textStyle="bold"/>
 
-<LinearLayout
-    android:id="@+id/foreign_keys_details"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content"
-    android:divider="?android:dividerHorizontal"
-    android:showDividers="middle"
-    android:orientation="vertical">
-</LinearLayout>
+    <LinearLayout
+        android:id="@+id/foreign_keys_details"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:divider="?android:dividerHorizontal"
+        android:orientation="vertical"
+        android:showDividers="middle">
+    </LinearLayout>
 
+    <TextView
+        android:layout_marginTop="8dp"
+        android:id="@+id/no_keys_to_accept"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@color/black87"
+        android:text="@string/no_keys_just_confirm"
+        android:textSize="?attr/TextSizeBody"/>
 </LinearLayout>

src/main/res/values/strings.xml 🔗

@@ -599,4 +599,5 @@
 	<string name="this_field_is_required">This field is required</string>
 	<string name="correct_message">Correct message</string>
 	<string name="send_corrected_message">Send corrected message</string>
+	<string name="no_keys_just_confirm">You already trust this contact. By selecting \'done\' you are just confirming that %s is part of this conference.</string>
 </resources>