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}