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