EnterJidDialog.java

  1package eu.siacs.conversations.ui;
  2
  3import android.app.Activity;
  4import android.app.Dialog;
  5import android.os.Bundle;
  6import android.text.Editable;
  7import android.text.TextWatcher;
  8import android.view.View;
  9import android.widget.ArrayAdapter;
 10
 11import androidx.annotation.NonNull;
 12import androidx.appcompat.app.AlertDialog;
 13import androidx.databinding.DataBindingUtil;
 14import androidx.fragment.app.DialogFragment;
 15
 16import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 17import com.google.common.base.Strings;
 18
 19import java.util.ArrayList;
 20import java.util.Arrays;
 21import java.util.Collection;
 22import java.util.Collections;
 23import java.util.List;
 24
 25import eu.siacs.conversations.R;
 26import eu.siacs.conversations.databinding.DialogEnterJidBinding;
 27import eu.siacs.conversations.services.XmppConnectionService;
 28import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
 29import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
 30import eu.siacs.conversations.ui.util.DelayedHintHelper;
 31import eu.siacs.conversations.xmpp.Jid;
 32
 33public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
 34
 35    private static final List<String> SUSPICIOUS_DOMAINS =
 36            Arrays.asList("conference", "muc", "room", "rooms", "chat");
 37
 38    private OnEnterJidDialogPositiveListener mListener = null;
 39
 40    private static final String TITLE_KEY = "title";
 41    private static final String POSITIVE_BUTTON_KEY = "positive_button";
 42    private static final String PREFILLED_JID_KEY = "prefilled_jid";
 43    private static final String ACCOUNT_KEY = "account";
 44    private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
 45    private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
 46    private static final String SANITY_CHECK_JID = "sanity_check_jid";
 47
 48    private KnownHostsAdapter knownHostsAdapter;
 49    private Collection<String> whitelistedDomains = Collections.emptyList();
 50
 51    private DialogEnterJidBinding binding;
 52    private AlertDialog dialog;
 53    private boolean sanityCheckJid = false;
 54
 55    private boolean issuedWarning = false;
 56
 57    public static EnterJidDialog newInstance(
 58            final ArrayList<String> activatedAccounts,
 59            final String title,
 60            final String positiveButton,
 61            final String prefilledJid,
 62            final String account,
 63            boolean allowEditJid,
 64            final boolean sanity_check_jid) {
 65        final EnterJidDialog dialog = new EnterJidDialog();
 66        Bundle bundle = new Bundle();
 67        bundle.putString(TITLE_KEY, title);
 68        bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
 69        bundle.putString(PREFILLED_JID_KEY, prefilledJid);
 70        bundle.putString(ACCOUNT_KEY, account);
 71        bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
 72        bundle.putStringArrayList(ACCOUNTS_LIST_KEY, activatedAccounts);
 73        bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
 74        dialog.setArguments(bundle);
 75        return dialog;
 76    }
 77
 78    @Override
 79    public void onActivityCreated(Bundle savedInstanceState) {
 80        super.onActivityCreated(savedInstanceState);
 81        setRetainInstance(true);
 82    }
 83
 84    @Override
 85    public void onStart() {
 86        super.onStart();
 87        final Activity activity = getActivity();
 88        if (activity instanceof XmppActivity
 89                && ((XmppActivity) activity).xmppConnectionService != null) {
 90            refreshKnownHosts();
 91        }
 92    }
 93
 94    @NonNull
 95    @Override
 96    public Dialog onCreateDialog(final Bundle savedInstanceState) {
 97        final var arguments = getArguments();
 98        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
 99        builder.setTitle(arguments.getString(TITLE_KEY));
100        binding =
101                DataBindingUtil.inflate(requireActivity().getLayoutInflater(), R.layout.dialog_enter_jid, null, false);
102        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
103        binding.jid.setAdapter(this.knownHostsAdapter);
104        binding.jid.addTextChangedListener(this);
105        final String prefilledJid = arguments.getString(PREFILLED_JID_KEY);
106        if (prefilledJid != null) {
107            binding.jid.append(prefilledJid);
108            if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) {
109                binding.jid.setFocusable(false);
110                binding.jid.setFocusableInTouchMode(false);
111                binding.jid.setClickable(false);
112                binding.jid.setCursorVisible(false);
113            }
114        }
115        sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
116
117        DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
118
119        final String account = getArguments().getString(ACCOUNT_KEY);
120        if (Strings.isNullOrEmpty(account)) {
121            StartConversationActivity.populateAccountSpinner(
122                    getActivity(),
123                    arguments.getStringArrayList(ACCOUNTS_LIST_KEY),
124                    binding.account);
125        } else {
126            final ArrayAdapter<String> adapter =
127                    new ArrayAdapter<>(requireActivity(), R.layout.item_autocomplete, new String[] {account});
128            binding.account.setText(account);
129            binding.account.setEnabled(false);
130            adapter.setDropDownViewResource(R.layout.item_autocomplete);
131            binding.account.setAdapter(adapter);
132        }
133
134        builder.setView(binding.getRoot());
135        builder.setNegativeButton(R.string.cancel, null);
136        builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
137        this.dialog = builder.create();
138
139        View.OnClickListener dialogOnClick =
140                v -> handleEnter(binding, account);
141
142        binding.jid.setOnEditorActionListener(
143                (v, actionId, event) -> {
144                    handleEnter(binding, account);
145                    return true;
146                });
147
148        dialog.show();
149        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
150        return dialog;
151    }
152
153    private void handleEnter(DialogEnterJidBinding binding, String account) {
154        final Jid accountJid;
155        if (!binding.account.isEnabled() && account == null) {
156            return;
157        }
158        try {
159                accountJid = Jid.ofEscaped((String) binding.account.getEditableText().toString());
160        } catch (final IllegalArgumentException e) {
161            return;
162        }
163        final Jid contactJid;
164        try {
165            contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim());
166        } catch (final IllegalArgumentException e) {
167            binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
168            return;
169        }
170
171        if (!issuedWarning && sanityCheckJid) {
172            if (contactJid.isDomainJid()) {
173                binding.jidLayout.setError(
174                        getActivity().getString(R.string.this_looks_like_a_domain));
175                dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
176                issuedWarning = true;
177                return;
178            }
179            if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
180                binding.jidLayout.setError(
181                        getActivity().getString(R.string.this_looks_like_channel));
182                dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
183                issuedWarning = true;
184                return;
185            }
186        }
187
188        if (mListener != null) {
189            try {
190                if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
191                    dialog.dismiss();
192                }
193            } catch (JidError error) {
194                binding.jidLayout.setError(error.toString());
195                dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
196                issuedWarning = false;
197            }
198        }
199    }
200
201    public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
202        this.mListener = listener;
203    }
204
205    @Override
206    public void onBackendConnected() {
207        refreshKnownHosts();
208    }
209
210    private void refreshKnownHosts() {
211        final Activity activity = getActivity();
212        if (activity instanceof XmppActivity) {
213            final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService;
214            if (service == null) {
215                return;
216            }
217            final Collection<String> hosts = service.getKnownHosts();
218            this.knownHostsAdapter.refresh(hosts);
219            this.whitelistedDomains = hosts;
220        }
221    }
222
223    @Override
224    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
225
226    @Override
227    public void onTextChanged(CharSequence s, int start, int before, int count) {}
228
229    @Override
230    public void afterTextChanged(Editable s) {
231        if (issuedWarning) {
232            dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
233            binding.jidLayout.setError(null);
234            issuedWarning = false;
235        }
236    }
237
238    public interface OnEnterJidDialogPositiveListener {
239        boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
240    }
241
242    public static class JidError extends Exception {
243        final String msg;
244
245        public JidError(final String msg) {
246            this.msg = msg;
247        }
248
249        @NonNull
250        public String toString() {
251            return msg;
252        }
253    }
254
255    @Override
256    public void onDestroyView() {
257        Dialog dialog = getDialog();
258        if (dialog != null && getRetainInstance()) {
259            dialog.setDismissMessage(null);
260        }
261        super.onDestroyView();
262    }
263
264    private boolean suspiciousSubDomain(String domain) {
265        if (this.whitelistedDomains.contains(domain)) {
266            return false;
267        }
268        final String[] parts = domain.split("\\.");
269        return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]);
270    }
271}