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}