Use a RecyclerView for list of gateway options

Stephen Paul Weber created

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.

Change summary

build.gradle                                                              |   2 
src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java |  10 
src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java               | 179 
src/main/res/layout/enter_jid_dialog.xml                                  |   5 
src/main/res/layout/enter_jid_dialog_gateway_list_item.xml                |  14 
5 files changed, 202 insertions(+), 8 deletions(-)

Detailed changes

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
 }

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) {

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<String> 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<GatewayListAdapter.ViewHolder> {
+        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<Pair<Contact,String>> 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<String, Pair<Jid,Presence>> getSelected() {
+            if(this.selected == 0) {
+                return null; // No gateway, just use direct JID entry
+            }
+
+            Pair<Contact,String> gateway = this.gateways.get(this.selected - 1);
+
+            Pair<Jid,Presence> presence = null;
+            for (Map.Entry<String,Presence> 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();
+        }
+    }
 }

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

@@ -22,6 +22,11 @@
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"/>
 
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/gateway_list"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content" />
+
         <com.google.android.material.textfield.TextInputLayout
             android:id="@+id/jid_layout"
             android:layout_width="match_parent"

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

@@ -0,0 +1,14 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:orientation="vertical"
+              android:paddingRight="5dp">
+    <ToggleButton
+        android:id="@+id/button"
+        android:gravity="center"
+        android:visibility="gone"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:textColor="?attr/edit_text_color"
+        android:textSize="?attr/TextSizeBody1" />
+</LinearLayout>