From d29b33863b3dbe7a02bb885351a0a550634446ec Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 10:10:55 -0500 Subject: [PATCH 01/11] Fetch jabber:iq:gateway prompt --- .../services/XmppConnectionService.java | 18 ++++++++++++++++++ .../xmpp/OnGatewayPromptResult.java | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1b2cc7a26..9c177f23a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -145,6 +145,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; +import eu.siacs.conversations.xmpp.OnGatewayPromptResult; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; @@ -4655,6 +4656,23 @@ public class XmppConnectionService extends Service { } } + public void fetchGatewayPrompt(Account account, final Jid jid, final OnGatewayPromptResult callback) { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(jid); + request.query("jabber:iq:gateway"); + sendIqPacket(account, request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + callback.onGatewayPromptResult(packet.query().findChildContent("prompt"), null); + } else { + Element error = packet.findChild("error"); + callback.onGatewayPromptResult(null, error == null ? null : error.findChildContent("text")); + } + } + }); + } + public void fetchCaps(Account account, final Jid jid, final Presence presence) { final Pair key = new Pair<>(presence.getHash(), presence.getVer()); final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java b/src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java new file mode 100644 index 000000000..7c903021c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +public interface OnGatewayPromptResult { + // if prompt is null, there was an error + // errorText may or may not be set + public void onGatewayPromptResult(String prompt, String errorText); +} From 49cbe7448e175329ea876bd450bbf52f65e7fe5c Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 12:22:33 -0500 Subject: [PATCH 02/11] Use a RecyclerView for list of gateway options If there are no gateways in the current account's roster, just show the old "Jabber ID" label. Otherwise show a list of toggle buttons. These buttons show the type of their gateway and change the JID input to not autocomplete and to show a hint matching the jabber:id:gateway prompt. This is just UI, submit behaviour is unchanged. List is not yet populated by this code. --- build.gradle | 2 + .../entities/ServiceDiscoveryResult.java | 10 +- .../conversations/ui/EnterJidDialog.java | 179 +++++++++++++++++- src/main/res/layout/enter_jid_dialog.xml | 5 + .../enter_jid_dialog_gateway_list_item.xml | 14 ++ 5 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 src/main/res/layout/enter_jid_dialog_gateway_list_item.xml diff --git a/build.gradle b/build.gradle index 6a182af6e..64b41ea1d 100644 --- a/build.gradle +++ b/build.gradle @@ -94,6 +94,8 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' implementation 'io.michaelrocks:libphonenumber-android:8.12.36' implementation 'io.github.nishkarsh:android-permissions:2.1.6' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'org.solovyev.android.views:linear-layout-manager:0.5@aar' implementation urlFile('https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar') // INSERT } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 8eccbe141..495cfaae4 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -168,15 +168,19 @@ public class ServiceDiscoveryResult { return this.features; } - public boolean hasIdentity(String category, String type) { + public Identity getIdentity(String category, String type) { for (Identity id : this.getIdentities()) { if ((category == null || id.getCategory().equals(category)) && (type == null || id.getType().equals(type))) { - return true; + return id; } } - return false; + return null; + } + + public boolean hasIdentity(String category, String type) { + return getIdentity(category, type) != null; } public String getExtendedDiscoInformation(String formType, String name) { diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 15ebdb0b7..0e26a9e4c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -2,31 +2,48 @@ package eu.siacs.conversations.ui; import android.app.Activity; import android.app.Dialog; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface; import android.os.Bundle; import android.text.Editable; +import android.text.InputType; import android.text.TextWatcher; +import android.util.Pair; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import org.solovyev.android.views.llm.LinearLayoutManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.EnterJidDialogBinding; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.util.DelayedHintHelper; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.OnGatewayPromptResult; public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { @@ -51,6 +68,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected private boolean sanityCheckJid = false; private boolean issuedWarning = false; + private GatewayListAdapter gatewayListAdapter = new GatewayListAdapter(); public static EnterJidDialog newInstance( final List activatedAccounts, @@ -129,6 +147,9 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected binding.account.setAdapter(adapter); } + binding.gatewayList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); + binding.gatewayList.setAdapter(gatewayListAdapter); + builder.setView(binding.getRoot()); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); @@ -157,11 +178,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected } try { if (Config.DOMAIN_LOCK != null) { - accountJid = - Jid.ofEscaped( - (String) binding.account.getSelectedItem(), - Config.DOMAIN_LOCK, - null); + accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); } else { accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); } @@ -276,4 +293,156 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected final String[] parts = domain.split("\\."); return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); } + + protected class GatewayListAdapter extends RecyclerView.Adapter { + protected class ViewHolder extends RecyclerView.ViewHolder { + protected ToggleButton button; + protected int index; + + public ViewHolder(View view, int i) { + super(view); + this.button = (ToggleButton) view.findViewById(R.id.button); + setIndex(i); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + button.setChecked(true); // Force visual not to flap to unchecked + setSelected(index); + } + }); + } + + public void setIndex(int i) { + this.index = i; + button.setChecked(selected == i); + } + + public void useButton(int res) { + button.setText(res); + button.setTextOff(button.getText()); + button.setTextOn(button.getText()); + button.setChecked(selected == this.index); + binding.gatewayList.setVisibility(View.VISIBLE); + button.setVisibility(View.VISIBLE); + } + + public void useButton(String txt) { + button.setTextOff(txt); + button.setTextOn(txt); + button.setChecked(selected == this.index); + binding.gatewayList.setVisibility(View.VISIBLE); + button.setVisibility(View.VISIBLE); + } + } + + protected List> gateways = new ArrayList(); + protected int selected = 0; + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { + View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.enter_jid_dialog_gateway_list_item, null); + return new ViewHolder(view, i); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, int i) { + viewHolder.setIndex(i); + + if(i == 0) { + if(getItemCount() < 2) { + binding.gatewayList.setVisibility(View.GONE); + } else { + viewHolder.useButton(R.string.account_settings_jabber_id); + } + } else { + viewHolder.useButton(getLabel(i)); + } + } + + @Override + public int getItemCount() { + return this.gateways.size() + 1; + } + + public void setSelected(int i) { + int old = this.selected; + this.selected = i; + + if(i == 0) { + binding.jid.setThreshold(1); + binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); + binding.jidLayout.setHint(R.string.account_settings_jabber_id); + } else { + binding.jid.setThreshold(999999); // do not autocomplete + binding.jid.setInputType(InputType.TYPE_CLASS_TEXT); + binding.jidLayout.setHint(this.gateways.get(i-1).second); + binding.jid.setHint(null); + binding.jid.setOnFocusChangeListener((v, hasFocus) -> {}); + } + + notifyItemChanged(old); + notifyItemChanged(i); + } + + public String getLabel(int i) { + if (i == 0) return null; + + for(Presence p : this.gateways.get(i-1).first.getPresences().getPresences()) { + ServiceDiscoveryResult.Identity id; + if(p.getServiceDiscoveryResult() != null && (id = p.getServiceDiscoveryResult().getIdentity("gateway", null)) != null) { + return id.getType(); + } + } + + return gateways.get(i-1).first.getDisplayName(); + } + + public String getSelectedLabel() { + return getLabel(selected); + } + + public Pair> getSelected() { + if(this.selected == 0) { + return null; // No gateway, just use direct JID entry + } + + Pair gateway = this.gateways.get(this.selected - 1); + + Pair presence = null; + for (Map.Entry e : gateway.first.getPresences().getPresencesMap().entrySet()) { + Presence p = e.getValue(); + if (p.getServiceDiscoveryResult() != null) { + if (p.getServiceDiscoveryResult().getFeatures().contains("jabber:iq:gateway")) { + if (e.getKey().equals("")) { + presence = new Pair<>(gateway.first.getJid(), p); + } else { + presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p); + } + break; + } + if (p.getServiceDiscoveryResult().hasIdentity("gateway", null)) { + if (e.getKey().equals("")) { + presence = new Pair<>(gateway.first.getJid(), p); + } else { + presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p); + } + } + } + } + + return presence == null ? null : new Pair(gateway.second, presence); + } + + public void clear() { + this.gateways.clear(); + notifyDataSetChanged(); + setSelected(0); + } + + public void add(Contact gateway, String prompt) { + binding.gatewayList.setVisibility(View.VISIBLE); + this.gateways.add(new Pair<>(gateway, prompt)); + notifyDataSetChanged(); + } + } } diff --git a/src/main/res/layout/enter_jid_dialog.xml b/src/main/res/layout/enter_jid_dialog.xml index cacb98a6f..f7d2bfbaa 100644 --- a/src/main/res/layout/enter_jid_dialog.xml +++ b/src/main/res/layout/enter_jid_dialog.xml @@ -22,6 +22,11 @@ android:layout_width="fill_parent" android:layout_height="wrap_content"/> + + + + From 017164056e32c739b5202aa4911f0825676492e6 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 13:06:05 -0500 Subject: [PATCH 03/11] Load gateways into UI --- .../conversations/ui/EnterJidDialog.java | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 0e26a9e4c..d54ce3d02 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -150,6 +150,34 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected binding.gatewayList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); binding.gatewayList.setAdapter(gatewayListAdapter); + binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView accountSpinner, View view, int position, long id) { + XmppActivity context = (XmppActivity) getActivity(); + if (context.xmppConnectionService == null || accountJid() == null) return; + + gatewayListAdapter.clear(); + final Account account = context.xmppConnectionService.findAccountByJid(accountJid()); + + for (final Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) { + context.xmppConnectionService.fetchGatewayPrompt(account, contact.getJid(), (final String prompt, String errorMessage) -> { + if (prompt == null) return; + + context.runOnUiThread(() -> { + gatewayListAdapter.add(contact, prompt); + }); + }); + } + } + } + + @Override + public void onNothingSelected(AdapterView accountSpinner) { + gatewayListAdapter.clear(); + } + }); + builder.setView(binding.getRoot()); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); @@ -171,20 +199,23 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected return dialog; } + protected Jid accountJid() { + try { + if (Config.DOMAIN_LOCK != null) { + return Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); + } else { + return Jid.ofEscaped((String) binding.account.getSelectedItem()); + } + } catch (final IllegalArgumentException e) { + return null; + } + } + private void handleEnter(EnterJidDialogBinding binding, String account) { - final Jid accountJid; if (!binding.account.isEnabled() && account == null) { return; } - try { - if (Config.DOMAIN_LOCK != null) { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); - } - } catch (final IllegalArgumentException e) { - return; - } + final Jid accountJid = accountJid(); final Jid contactJid; try { contactJid = Jid.ofEscaped(binding.jid.getText().toString()); From 1d0e2858160dbbe9a90b9e0506077240d00b7711 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 13:49:32 -0500 Subject: [PATCH 04/11] Change input type based on gateway type --- .../conversations/ui/EnterJidDialog.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index d54ce3d02..83229453a 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -405,7 +405,16 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected binding.jidLayout.setHint(R.string.account_settings_jabber_id); } else { binding.jid.setThreshold(999999); // do not autocomplete - binding.jid.setInputType(InputType.TYPE_CLASS_TEXT); + + String type = getType(i); + if (type.equals("pstn") || type.equals("sms")) { + binding.jid.setInputType(InputType.TYPE_CLASS_PHONE); + } else if (type.equals("email") || type.equals("sip")) { + binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + } else { + binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } + binding.jidLayout.setHint(this.gateways.get(i-1).second); binding.jid.setHint(null); binding.jid.setOnFocusChangeListener((v, hasFocus) -> {}); @@ -418,6 +427,15 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected public String getLabel(int i) { if (i == 0) return null; + String type = getType(i); + if (type != null) return type; + + return gateways.get(i-1).first.getDisplayName(); + } + + public String getType(int i) { + if (i == 0) return null; + for(Presence p : this.gateways.get(i-1).first.getPresences().getPresences()) { ServiceDiscoveryResult.Identity id; if(p.getServiceDiscoveryResult() != null && (id = p.getServiceDiscoveryResult().getIdentity("gateway", null)) != null) { @@ -425,11 +443,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected } } - return gateways.get(i-1).first.getDisplayName(); - } - - public String getSelectedLabel() { - return getLabel(selected); + return null; } public Pair> getSelected() { From 7845ded2d3f8ee679f2d349a235c01ba80a37962 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 15:08:07 -0500 Subject: [PATCH 05/11] Try all gateway translations options Send jabber:iq:gateway if we're responding to that and fetch translated response as the JID. Otherwise use JID escaping if supported. Otherwise fall back to the dumb ancient % escaping. --- .../services/XmppConnectionService.java | 27 ++--- .../conversations/ui/EnterJidDialog.java | 98 ++++++++++++------- ...PromptResult.java => OnGatewayResult.java} | 4 +- 3 files changed, 78 insertions(+), 51 deletions(-) rename src/main/java/eu/siacs/conversations/xmpp/{OnGatewayPromptResult.java => OnGatewayResult.java} (52%) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 9c177f23a..6c976bf53 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -145,7 +145,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; -import eu.siacs.conversations.xmpp.OnGatewayPromptResult; +import eu.siacs.conversations.xmpp.OnGatewayResult; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; @@ -4656,19 +4656,20 @@ public class XmppConnectionService extends Service { } } - public void fetchGatewayPrompt(Account account, final Jid jid, final OnGatewayPromptResult callback) { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) { + IqPacket request = new IqPacket(input == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); request.setTo(jid); - request.query("jabber:iq:gateway"); - sendIqPacket(account, request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onGatewayPromptResult(packet.query().findChildContent("prompt"), null); - } else { - Element error = packet.findChild("error"); - callback.onGatewayPromptResult(null, error == null ? null : error.findChildContent("text")); - } + Element query = request.query("jabber:iq:gateway"); + if (input != null) { + Element prompt = query.addChild("prompt"); + prompt.setContent(input); + } + sendIqPacket(account, request, (Account acct, IqPacket packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null); + } else { + Element error = packet.findChild("error"); + callback.onGatewayResult(null, error == null ? null : error.findChildContent("text")); } }); } diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 83229453a..e0cb5cd48 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -43,7 +43,7 @@ import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.util.DelayedHintHelper; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnGatewayPromptResult; +import eu.siacs.conversations.xmpp.OnGatewayResult; public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { @@ -161,7 +161,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) { - context.xmppConnectionService.fetchGatewayPrompt(account, contact.getJid(), (final String prompt, String errorMessage) -> { + context.xmppConnectionService.fetchFromGateway(account, contact.getJid(), null, (final String prompt, String errorMessage) -> { if (prompt == null) return; context.runOnUiThread(() -> { @@ -216,41 +216,67 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected return; } final Jid accountJid = accountJid(); - final Jid contactJid; - try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString()); - } catch (final IllegalArgumentException e) { - binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); - return; - } - - if (!issuedWarning && sanityCheckJid) { - if (contactJid.isDomainJid()) { - binding.jidLayout.setError( - getActivity().getString(R.string.this_looks_like_a_domain)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { - binding.jidLayout.setError( - getActivity().getString(R.string.this_looks_like_channel)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - } - - if (mListener != null) { - try { - if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { - dialog.dismiss(); + final OnGatewayResult finish = (final String jidString, final String errorMessage) -> { + getActivity().runOnUiThread(() -> { + if (errorMessage != null) { + binding.jidLayout.setError(errorMessage); + return; } - } catch (JidError error) { - binding.jidLayout.setError(error.toString()); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - issuedWarning = false; - } + if (jidString == null) { + binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); + return; + } + + final Jid contactJid; + try { + contactJid = Jid.ofEscaped(jidString); + } catch (final IllegalArgumentException e) { + binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); + return; + } + + if (!issuedWarning && sanityCheckJid) { + if (contactJid.isDomainJid()) { + binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { + binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + } + + if (mListener != null) { + try { + if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { + dialog.dismiss(); + } + } catch (JidError error) { + binding.jidLayout.setError(error.toString()); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); + issuedWarning = false; + } + } + }); + }; + + Pair> p = gatewayListAdapter.getSelected(); + + if (p == null) { + finish.onGatewayResult(binding.jid.getText().toString(), null); + } else if (p.first != null) { // Gateway already responsed to jabber:iq:gateway once + final Account acct = ((XmppActivity) getActivity()).xmppConnectionService.findAccountByJid(accountJid); + ((XmppActivity) getActivity()).xmppConnectionService.fetchFromGateway(acct, p.second.first, binding.jid.getText().toString(), finish); + } else if (p.second.first.isDomainJid() && p.second.second.getServiceDiscoveryResult().getFeatures().contains("jid\\20escaping")) { + finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString(), p.second.first.getDomain().toString()).toString(), null); + } else if (p.second.first.isDomainJid()) { + finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString().replace("@", "%"), p.second.first.getDomain().toString()).toString(), null); + } else { + finish.onGatewayResult(null, null); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java b/src/main/java/eu/siacs/conversations/xmpp/OnGatewayResult.java similarity index 52% rename from src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java rename to src/main/java/eu/siacs/conversations/xmpp/OnGatewayResult.java index 7c903021c..fd95d0761 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/OnGatewayPromptResult.java +++ b/src/main/java/eu/siacs/conversations/xmpp/OnGatewayResult.java @@ -1,7 +1,7 @@ package eu.siacs.conversations.xmpp; -public interface OnGatewayPromptResult { +public interface OnGatewayResult { // if prompt is null, there was an error // errorText may or may not be set - public void onGatewayPromptResult(String prompt, String errorText); + public void onGatewayResult(String prompt, String errorText); } From 55833f0481a80ecbf9b65840714dd2f3ba16412d Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 15:14:45 -0500 Subject: [PATCH 06/11] Parse phone numbers using local settings before asking gateway --- .../eu/siacs/conversations/ui/EnterJidDialog.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index e0cb5cd48..e1d7a27b6 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -29,6 +29,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; + +import io.michaelrocks.libphonenumber.android.NumberParseException; import org.solovyev.android.views.llm.LinearLayoutManager; import eu.siacs.conversations.Config; @@ -42,6 +44,7 @@ import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.util.DelayedHintHelper; +import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnGatewayResult; @@ -265,6 +268,14 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected }; Pair> p = gatewayListAdapter.getSelected(); + final String type = gatewayListAdapter.getSelectedType(); + + // Resolve based on local settings before submission + if (type.equals("pstn") || type.equals("sms")) { + try { + binding.jid.setText(PhoneNumberUtilWrapper.normalize(getActivity(), binding.jid.getText().toString())); + } catch (NumberParseException | NullPointerException e) { } + } if (p == null) { finish.onGatewayResult(binding.jid.getText().toString(), null); @@ -472,6 +483,10 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected return null; } + public String getSelectedType() { + return getType(selected); + } + public Pair> getSelected() { if(this.selected == 0) { return null; // No gateway, just use direct JID entry From 4444909d493816957143235f5299610aa1c3260e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 23:10:13 -0500 Subject: [PATCH 07/11] Switch to AndroidX LinearLayoutManager --- build.gradle | 1 - src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 64b41ea1d..caf02dcda 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,6 @@ dependencies { implementation 'io.michaelrocks:libphonenumber-android:8.12.36' implementation 'io.github.nishkarsh:android-permissions:2.1.6' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'org.solovyev.android.views:linear-layout-manager:0.5@aar' implementation urlFile('https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar') // INSERT } diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index e1d7a27b6..7c585d74c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -22,6 +22,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; import java.util.ArrayList; import java.util.Arrays; @@ -31,7 +32,6 @@ import java.util.List; import java.util.Map; import io.michaelrocks.libphonenumber.android.NumberParseException; -import org.solovyev.android.views.llm.LinearLayoutManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; From 58cf187f3183bfbe37903a124bfd3662ea67b2f8 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 23:10:43 -0500 Subject: [PATCH 08/11] Sort gateways --- .../conversations/ui/EnterJidDialog.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 7c585d74c..87bf169f0 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -461,19 +461,27 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected notifyItemChanged(i); } + public String getLabel(Contact gateway) { + String type = getType(gateway); + if (type != null) return type; + + return gateway.getDisplayName(); + } + public String getLabel(int i) { if (i == 0) return null; - String type = getType(i); - if (type != null) return type; - - return gateways.get(i-1).first.getDisplayName(); + return getLabel(this.gateways.get(i-1).first); } public String getType(int i) { if (i == 0) return null; - for(Presence p : this.gateways.get(i-1).first.getPresences().getPresences()) { + return getType(this.gateways.get(i-1).first); + } + + public String getType(Contact gateway) { + for(Presence p : gateway.getPresences().getPresences()) { ServiceDiscoveryResult.Identity id; if(p.getServiceDiscoveryResult() != null && (id = p.getServiceDiscoveryResult().getIdentity("gateway", null)) != null) { return id.getType(); @@ -528,6 +536,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected public void add(Contact gateway, String prompt) { binding.gatewayList.setVisibility(View.VISIBLE); this.gateways.add(new Pair<>(gateway, prompt)); + Collections.sort(this.gateways, (x, y) -> getLabel(x.first).compareTo(getLabel(y.first))); notifyDataSetChanged(); } } From 82bf15e040bea87d9c4fccc345ba104ae9470ced Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 7 Mar 2022 23:10:57 -0500 Subject: [PATCH 09/11] Show identity category gateway even without jabber:iq:gateway prompt --- src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 87bf169f0..696d61483 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -165,7 +165,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) { context.xmppConnectionService.fetchFromGateway(account, contact.getJid(), null, (final String prompt, String errorMessage) -> { - if (prompt == null) return; + if (prompt == null && !contact.getPresences().anyIdentity("gateway", null)) return; context.runOnUiThread(() -> { gatewayListAdapter.add(contact, prompt); From 06a98a6da22b084ca99fc6ff8e5ee92ef9c6a3c3 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 9 Mar 2022 15:56:11 -0500 Subject: [PATCH 10/11] Move GONE/VISIBLE to the controller instead of the model --- .../conversations/ui/EnterJidDialog.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 696d61483..104d3f4db 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -152,6 +152,8 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected binding.gatewayList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); binding.gatewayList.setAdapter(gatewayListAdapter); + gatewayListAdapter.setOnEmpty(() -> binding.gatewayList.setVisibility(View.GONE)); + gatewayListAdapter.setOnNonEmpty(() -> binding.gatewayList.setVisibility(View.VISIBLE)); binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -405,6 +407,8 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected protected List> gateways = new ArrayList(); protected int selected = 0; + protected Runnable onEmpty = () -> {}; + protected Runnable onNonEmpty = () -> {}; @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { @@ -417,11 +421,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected viewHolder.setIndex(i); if(i == 0) { - if(getItemCount() < 2) { - binding.gatewayList.setVisibility(View.GONE); - } else { - viewHolder.useButton(R.string.account_settings_jabber_id); - } + viewHolder.useButton(R.string.account_settings_jabber_id); } else { viewHolder.useButton(getLabel(i)); } @@ -527,14 +527,23 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected return presence == null ? null : new Pair(gateway.second, presence); } + public void setOnEmpty(Runnable r) { + onEmpty = r; + } + + public void setOnNonEmpty(Runnable r) { + onNonEmpty = r; + } + public void clear() { - this.gateways.clear(); + gateways.clear(); + onEmpty.run(); notifyDataSetChanged(); setSelected(0); } public void add(Contact gateway, String prompt) { - binding.gatewayList.setVisibility(View.VISIBLE); + if (getItemCount() < 2) onNonEmpty.run(); this.gateways.add(new Pair<>(gateway, prompt)); Collections.sort(this.gateways, (x, y) -> getLabel(x.first).compareTo(getLabel(y.first))); notifyDataSetChanged(); From 3409431532efe95cc4f1240e337ebd7aec6221a8 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 9 Mar 2022 16:09:31 -0500 Subject: [PATCH 11/11] Improve example hinting --- .../conversations/ui/EnterJidDialog.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 104d3f4db..3a1850de1 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -440,21 +440,32 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected binding.jid.setThreshold(1); binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); binding.jidLayout.setHint(R.string.account_settings_jabber_id); + + if(binding.jid.hasFocus()) { + binding.jid.setHint(R.string.account_settings_example_jabber_id); + } else { + DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); + } } else { binding.jid.setThreshold(999999); // do not autocomplete + binding.jid.setHint(null); + binding.jid.setOnFocusChangeListener((v, hasFocus) -> {}); + binding.jidLayout.setHint(this.gateways.get(i-1).second); String type = getType(i); if (type.equals("pstn") || type.equals("sms")) { binding.jid.setInputType(InputType.TYPE_CLASS_PHONE); } else if (type.equals("email") || type.equals("sip")) { binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + + if(binding.jid.hasFocus()) { + binding.jid.setHint(R.string.account_settings_example_jabber_id); + } else { + DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); + } } else { binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } - - binding.jidLayout.setHint(this.gateways.get(i-1).second); - binding.jid.setHint(null); - binding.jid.setOnFocusChangeListener((v, hasFocus) -> {}); } notifyItemChanged(old);