ask for resource and use jingle direct init when JMI is not available. fixes #3751

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Presences.java                   | 302 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java              |  22 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                |  25 
src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java             | 182 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java  |  10 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java      |  16 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java            |  17 
8 files changed, 346 insertions(+), 232 deletions(-)

Detailed changes

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

@@ -9,146 +9,164 @@ import java.util.List;
 import java.util.Map;
 
 public class Presences {
-	private final Hashtable<String, Presence> presences = new Hashtable<>();
-
-	public List<Presence> getPresences() {
-		synchronized (this.presences) {
-			return new ArrayList<>(this.presences.values());
-		}
-	}
-
-	public Presence get(String resource) {
-		synchronized (this.presences) {
-			return this.presences.get(resource);
-		}
-	}
-
-	public void updatePresence(String resource, Presence presence) {
-		synchronized (this.presences) {
-			this.presences.put(resource, presence);
-		}
-	}
-
-	public void removePresence(String resource) {
-		synchronized (this.presences) {
-			this.presences.remove(resource);
-		}
-	}
-
-	public void clearPresences() {
-		synchronized (this.presences) {
-			this.presences.clear();
-		}
-	}
-
-	public Presence.Status getShownStatus() {
-		Presence.Status status = Presence.Status.OFFLINE;
-		synchronized (this.presences) {
-			for(Presence p : presences.values()) {
-				if (p.getStatus() == Presence.Status.DND) {
-					return p.getStatus();
-				} else if (p.getStatus().compareTo(status) < 0){
-					status = p.getStatus();
-				}
-			}
-		}
-		return status;
-	}
-
-	public int size() {
-		synchronized (this.presences) {
-			return presences.size();
-		}
-	}
-
-	public String[] toResourceArray() {
-		synchronized (this.presences) {
-			final String[] presencesArray = new String[presences.size()];
-			presences.keySet().toArray(presencesArray);
-			return presencesArray;
-		}
-	}
-
-	public List<PresenceTemplate> asTemplates() {
-		synchronized (this.presences) {
-			ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
-			for(Presence p : presences.values()) {
-				if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
-					templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
-				}
-			}
-			return templates;
-		}
-	}
-
-	public boolean has(String presence) {
-		synchronized (this.presences) {
-			return presences.containsKey(presence);
-		}
-	}
-
-	public List<String> getStatusMessages() {
-		ArrayList<String> messages = new ArrayList<>();
-		synchronized (this.presences) {
-			for(Presence presence : this.presences.values()) {
-				String message = presence.getMessage() == null ? null : presence.getMessage().trim();
-				if (message != null && !message.isEmpty() && !messages.contains(message)) {
-					messages.add(message);
-				}
-			}
-		}
-		return messages;
-	}
-
-	public boolean allOrNonSupport(String namespace) {
-		synchronized (this.presences) {
-			for(Presence presence : this.presences.values()) {
-				ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
-				if (disco == null || !disco.getFeatures().contains(namespace)) {
-					return false;
-				}
-			}
-		}
-		return true;
-	}
-
-	public Pair<Map<String, String>,Map<String,String>> toTypeAndNameMap() {
-		Map<String,String> typeMap = new HashMap<>();
-		Map<String,String> nameMap = new HashMap<>();
-		synchronized (this.presences) {
-			for(Map.Entry<String,Presence> presenceEntry : this.presences.entrySet()) {
-				String resource = presenceEntry.getKey();
-				Presence presence = presenceEntry.getValue();
-				ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
-				if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
-					ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
-					String type = identity.getType();
-					String name = identity.getName();
-					if (type != null) {
-						typeMap.put(resource,type);
-					}
-					if (name != null) {
-						nameMap.put(resource, nameWithoutVersion(name));
-					}
-				}
-			}
-		}
-		return new Pair<>(typeMap,nameMap);
-	}
-
-	private static String nameWithoutVersion(String name) {
-		String[] parts = name.split(" ");
-		if (parts.length > 1 && Character.isDigit(parts[parts.length -1].charAt(0))) {
-			StringBuilder output = new StringBuilder();
-			for(int i = 0; i < parts.length -1; ++i) {
-				if (output.length() != 0) {
-					output.append(' ');
-				}
-				output.append(parts[i]);
-			}
-			return output.toString();
-		} else {
-			return name;
-		}
-	}
+    private final Hashtable<String, Presence> presences = new Hashtable<>();
+
+    private static String nameWithoutVersion(String name) {
+        String[] parts = name.split(" ");
+        if (parts.length > 1 && Character.isDigit(parts[parts.length - 1].charAt(0))) {
+            StringBuilder output = new StringBuilder();
+            for (int i = 0; i < parts.length - 1; ++i) {
+                if (output.length() != 0) {
+                    output.append(' ');
+                }
+                output.append(parts[i]);
+            }
+            return output.toString();
+        } else {
+            return name;
+        }
+    }
+
+    public List<Presence> getPresences() {
+        synchronized (this.presences) {
+            return new ArrayList<>(this.presences.values());
+        }
+    }
+
+    public Map<String, Presence> getPresencesMap() {
+        synchronized (this.presences) {
+            return new HashMap<>(this.presences);
+        }
+    }
+
+    public Presence get(String resource) {
+        synchronized (this.presences) {
+            return this.presences.get(resource);
+        }
+    }
+
+    public void updatePresence(String resource, Presence presence) {
+        synchronized (this.presences) {
+            this.presences.put(resource, presence);
+        }
+    }
+
+    public void removePresence(String resource) {
+        synchronized (this.presences) {
+            this.presences.remove(resource);
+        }
+    }
+
+    public void clearPresences() {
+        synchronized (this.presences) {
+            this.presences.clear();
+        }
+    }
+
+    public Presence.Status getShownStatus() {
+        Presence.Status status = Presence.Status.OFFLINE;
+        synchronized (this.presences) {
+            for (Presence p : presences.values()) {
+                if (p.getStatus() == Presence.Status.DND) {
+                    return p.getStatus();
+                } else if (p.getStatus().compareTo(status) < 0) {
+                    status = p.getStatus();
+                }
+            }
+        }
+        return status;
+    }
+
+    public int size() {
+        synchronized (this.presences) {
+            return presences.size();
+        }
+    }
+
+    public String[] toResourceArray() {
+        synchronized (this.presences) {
+            final String[] presencesArray = new String[presences.size()];
+            presences.keySet().toArray(presencesArray);
+            return presencesArray;
+        }
+    }
+
+    public List<PresenceTemplate> asTemplates() {
+        synchronized (this.presences) {
+            ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
+            for (Presence p : presences.values()) {
+                if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
+                    templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
+                }
+            }
+            return templates;
+        }
+    }
+
+    public boolean has(String presence) {
+        synchronized (this.presences) {
+            return presences.containsKey(presence);
+        }
+    }
+
+    public List<String> getStatusMessages() {
+        ArrayList<String> messages = new ArrayList<>();
+        synchronized (this.presences) {
+            for (Presence presence : this.presences.values()) {
+                String message = presence.getMessage() == null ? null : presence.getMessage().trim();
+                if (message != null && !message.isEmpty() && !messages.contains(message)) {
+                    messages.add(message);
+                }
+            }
+        }
+        return messages;
+    }
+
+    public boolean allOrNonSupport(String namespace) {
+        synchronized (this.presences) {
+            for (Presence presence : this.presences.values()) {
+                ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
+                if (disco == null || !disco.getFeatures().contains(namespace)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public boolean anySupport(final String namespace) {
+        synchronized (this.presences) {
+            for (Presence presence : this.presences.values()) {
+                ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
+                if (disco != null && disco.getFeatures().contains(namespace)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
+        Map<String, String> typeMap = new HashMap<>();
+        Map<String, String> nameMap = new HashMap<>();
+        synchronized (this.presences) {
+            for (Map.Entry<String, Presence> presenceEntry : this.presences.entrySet()) {
+                String resource = presenceEntry.getKey();
+                Presence presence = presenceEntry.getValue();
+                ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
+                if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
+                    ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
+                    String type = identity.getType();
+                    String name = identity.getName();
+                    if (type != null) {
+                        typeMap.put(resource, type);
+                    }
+                    if (name != null) {
+                        nameMap.put(resource, nameWithoutVersion(name));
+                    }
+                }
+            }
+        }
+        return new Pair<>(typeMap, nameMap);
+    }
 }

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

@@ -116,6 +116,7 @@ import eu.siacs.conversations.utils.QuickLoader;
 import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.TimeFrameUtils;
 import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.chatstate.ChatState;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
@@ -1342,11 +1343,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
             return;
         }
+
         final Contact contact = conversation.getContact();
+        if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
+            triggerRtpSession(contact.getAccount(),contact.getJid().asBareJid(),action);
+        } else {
+            final RtpCapability.Capability capability;
+            if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) {
+                capability = RtpCapability.Capability.VIDEO;
+            } else {
+                capability = RtpCapability.Capability.AUDIO;
+            }
+            PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> {
+                triggerRtpSession(contact.getAccount(), fullJid, action);
+            });
+        }
+    }
+
+    private void triggerRtpSession(final Account account, final Jid with, final String action) {
         final Intent intent = new Intent(activity, RtpSessionActivity.class);
         intent.setAction(action);
-        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString());
-        intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString());
+        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
+        intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
         startActivity(intent);

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

@@ -51,6 +51,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.MainThreadExecutor;
 import eu.siacs.conversations.utils.PermissionUtils;
 import eu.siacs.conversations.utils.TimeFrameUtils;
+import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
@@ -306,7 +307,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
 
     private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
         checkMicrophoneAvailability();
-        xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
+        if (with.isBareJid()) {
+            xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
+        } else {
+            final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media);
+            initializeActivityWithRunningRtpSession(account, with, sessionId);
+            resetIntent(account, with, sessionId);
+        }
         putScreenInCallMode(media);
     }
 
@@ -444,8 +451,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
         return false;
     }
 
-    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
+    private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
         runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
+        resetIntent(account, with, sessionId);
+    }
+
+    private void resetIntent(final Account account, final Jid with, final String sessionId) {
         final Intent intent = new Intent(Intent.ACTION_VIEW);
         intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
         intent.putExtra(EXTRA_WITH, with.toEscapedString());
@@ -838,7 +849,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     }
 
     private void retry(View view) {
-        Log.d(Config.LOGTAG, "attempting retry");
         final Intent intent = getIntent();
         final Account account = extractAccount(intent);
         final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
@@ -846,6 +856,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
         final String action = intent.getAction();
         final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
         this.rtpConnectionReference = null;
+        Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
         proposeJingleRtpSession(account, with, media);
     }
 
@@ -899,7 +910,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
                 return;
             }
             //this happens when going from proposed session to actual session
-            reInitializeActivityWithRunningRapSession(account, with, sessionId);
+            reInitializeActivityWithRunningRtpSession(account, with, sessionId);
             return;
         }
         final AbstractJingleConnection.Id id = requireRtpConnection().getId();
@@ -976,8 +987,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
 
     private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
         final Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
         intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
+        if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
+            intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
+        } else {
+            intent.putExtra(EXTRA_WITH, with.toEscapedString());
+        }
         intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
         intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
         setIntent(intent);

src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java 🔗

@@ -44,92 +44,110 @@ import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Presences;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.jingle.RtpCapability;
 
 public class PresenceSelector {
 
-	public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
-		final Contact contact = conversation.getContact();
-		final Presences presences = contact.getPresences();
-		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-		builder.setTitle(activity.getString(R.string.choose_presence));
-		final String[] resourceArray = presences.toResourceArray();
-		Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
-		final Map<String, String> resourceTypeMap = typeAndName.first;
-		final Map<String, String> resourceNameMap = typeAndName.second;
-		final String[] readableIdentities = new String[resourceArray.length];
-		final AtomicInteger selectedResource = new AtomicInteger(0);
-		for (int i = 0; i < resourceArray.length; ++i) {
-			String resource = resourceArray[i];
-			if (resource.equals(contact.getLastResource())) {
-				selectedResource.set(i);
-			}
-			String type = resourceTypeMap.get(resource);
-			String name = resourceNameMap.get(resource);
-			if (type != null) {
-				if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
-					readableIdentities[i] = translateType(activity, type);
-				} else if (name != null) {
-					if (Collections.frequency(resourceNameMap.values(), name) == 1
-							|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
-						readableIdentities[i] = translateType(activity, type) + "  (" + name + ")";
-					} else {
-						readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")";
-					}
-				} else {
-					readableIdentities[i] = translateType(activity, type) + " (" + resource + ")";
-				}
-			} else {
-				readableIdentities[i] = resource;
-			}
-		}
-		builder.setSingleChoiceItems(readableIdentities,
-				selectedResource.get(),
-				(dialog, which) -> selectedResource.set(which));
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.ok, (dialog, which) -> {
-			try {
-				Jid next = Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]);
-				conversation.setNextCounterpart(next);
-			} catch (IllegalArgumentException e) {
-				conversation.setNextCounterpart(null);
-			}
-			listener.onPresenceSelected();
-		});
-		builder.create().show();
-	}
+    public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
+        final Contact contact = conversation.getContact();
+        final String[] resourceArray = contact.getPresences().toResourceArray();
+        showPresenceSelectionDialog(activity, contact, resourceArray, fullJid -> {
+            conversation.setNextCounterpart(fullJid);
+            listener.onPresenceSelected();
+        });
+    }
 
-	public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-		builder.setTitle(conversation.getContact().getJid().toString());
-		builder.setMessage(R.string.without_mutual_presence_updates);
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
-			conversation.setNextCounterpart(null);
-			if (listener != null) {
-				listener.onPresenceSelected();
-			}
-		});
-		builder.create().show();
-	}
+    public static void selectFullJidForDirectRtpConnection(final Activity activity, final Contact contact, final RtpCapability.Capability required, final OnFullJidSelected onFullJidSelected) {
+        final String[] resources = RtpCapability.filterPresences(contact, required);
+        if (resources.length == 1) {
+            onFullJidSelected.onFullJidSelected(contact.getJid().withResource(resources[0]));
+        } else {
+            showPresenceSelectionDialog(activity, contact, resources, onFullJidSelected);
+        }
+    }
 
-	private static String translateType(Context context, String type) {
-		switch (type.toLowerCase()) {
-			case "pc":
-				return context.getString(R.string.type_pc);
-			case "phone":
-				return context.getString(R.string.type_phone);
-			case "tablet":
-				return context.getString(R.string.type_tablet);
-			case "web":
-				return context.getString(R.string.type_web);
-			case "console":
-				return context.getString(R.string.type_console);
-			default:
-				return type;
-		}
-	}
+    private static void showPresenceSelectionDialog(final Activity activity, final Contact contact, final String[] resourceArray, final OnFullJidSelected onFullJidSelected) {
+        final Presences presences = contact.getPresences();
+        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        builder.setTitle(activity.getString(R.string.choose_presence));
+        Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
+        final Map<String, String> resourceTypeMap = typeAndName.first;
+        final Map<String, String> resourceNameMap = typeAndName.second;
+        final String[] readableIdentities = new String[resourceArray.length];
+        final AtomicInteger selectedResource = new AtomicInteger(0);
+        for (int i = 0; i < resourceArray.length; ++i) {
+            String resource = resourceArray[i];
+            if (resource.equals(contact.getLastResource())) {
+                selectedResource.set(i);
+            }
+            String type = resourceTypeMap.get(resource);
+            String name = resourceNameMap.get(resource);
+            if (type != null) {
+                if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
+                    readableIdentities[i] = translateType(activity, type);
+                } else if (name != null) {
+                    if (Collections.frequency(resourceNameMap.values(), name) == 1
+                            || CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
+                        readableIdentities[i] = translateType(activity, type) + "  (" + name + ")";
+                    } else {
+                        readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")";
+                    }
+                } else {
+                    readableIdentities[i] = translateType(activity, type) + " (" + resource + ")";
+                }
+            } else {
+                readableIdentities[i] = resource;
+            }
+        }
+        builder.setSingleChoiceItems(readableIdentities,
+                selectedResource.get(),
+                (dialog, which) -> selectedResource.set(which));
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(
+                R.string.ok,
+                (dialog, which) -> onFullJidSelected.onFullJidSelected(
+                        Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()])
+                )
+        );
+        builder.create().show();
+    }
 
-	public interface OnPresenceSelected {
-		void onPresenceSelected();
-	}
+    public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        builder.setTitle(conversation.getContact().getJid().toString());
+        builder.setMessage(R.string.without_mutual_presence_updates);
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
+            conversation.setNextCounterpart(null);
+            if (listener != null) {
+                listener.onPresenceSelected();
+            }
+        });
+        builder.create().show();
+    }
+
+    private static String translateType(Context context, String type) {
+        switch (type.toLowerCase()) {
+            case "pc":
+                return context.getString(R.string.type_pc);
+            case "phone":
+                return context.getString(R.string.type_phone);
+            case "tablet":
+                return context.getString(R.string.type_tablet);
+            case "web":
+                return context.getString(R.string.type_web);
+            case "console":
+                return context.getString(R.string.type_console);
+            default:
+                return type;
+        }
+    }
+
+    public interface OnPresenceSelected {
+        void onPresenceSelected();
+    }
+
+    public interface OnFullJidSelected {
+        void onFullJidSelected(Jid jid);
+    }
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java 🔗

@@ -62,6 +62,10 @@ public abstract class AbstractJingleConnection {
             return new Id(account, with, sessionId);
         }
 
+        public static Id of(Account account, Jid with) {
+            return new Id(account, with, JingleConnectionManager.nextRandomId());
+        }
+
         public static Id of(Message message) {
             return new Id(
                     message.getConversation().getAccount(),

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -11,6 +11,7 @@ import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableSet;
+import com.google.j2objc.annotations.Weak;
 
 import java.lang.ref.WeakReference;
 import java.security.SecureRandom;
@@ -523,6 +524,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         mXmppConnectionService.sendMessagePacket(account, messagePacket);
     }
 
+    public String initializeRtpSession(final Account account, final Jid with, final Set<Media> media) {
+        final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
+        final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
+        rtpConnection.setProposedMedia(media);
+        this.connections.put(id, rtpConnection);
+        rtpConnection.sendSessionInitiate();
+        return id.sessionId;
+    }
+
     public void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
         synchronized (this.rtpSessionProposals) {
             for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -640,6 +640,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
+    public void sendSessionInitiate() {
+        sendSessionInitiate(this.proposedMedia, State.SESSION_INITIALIZED);
+    }
+
     private void sendSessionInitiate(final Set<Media> media, final State targetState) {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
         discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
@@ -781,6 +785,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     public RtpEndUserState getEndUserState() {
         switch (this.state) {
+            case NULL:
             case PROPOSED:
             case SESSION_INITIALIZED:
                 if (isInitiator()) {
@@ -836,10 +841,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     public Set<Media> getMedia() {
         final State current = getState();
         if (current == State.NULL) {
+            if (isInitiator()) {
+                return Preconditions.checkNotNull(
+                        this.proposedMedia,
+                        "RTP connection has not been initialized properly"
+                );
+            }
             throw new IllegalStateException("RTP connection has not been initialized yet");
         }
         if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
-            return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly");
+            return Preconditions.checkNotNull(
+                    this.proposedMedia,
+                    "RTP connection has not been initialized properly"
+            );
         }
         final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
         if (initiatorContentMap != null) {

src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java 🔗

@@ -1,8 +1,10 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Presence;
@@ -37,6 +39,21 @@ public class RtpCapability {
         return Capability.NONE;
     }
 
+    public static String[] filterPresences(final Contact contact, Capability required) {
+        final Presences presences = contact.getPresences();
+        final ArrayList<String> resources = new ArrayList<>();
+        for(final Map.Entry<String,Presence> presence : presences.getPresencesMap().entrySet()) {
+            final Capability capability = check(presence.getValue());
+            if (capability == Capability.NONE) {
+                continue;
+            }
+            if (required == Capability.AUDIO || capability == required) {
+                resources.add(presence.getKey());
+            }
+        }
+        return resources.toArray(new String[0]);
+    }
+
     public static Capability check(final Contact contact) {
         final Presences presences = contact.getPresences();
         Capability result = Capability.NONE;