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