1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.app.Dialog;
5import android.content.Context;
6import android.content.DialogInterface;
7import android.databinding.DataBindingUtil;
8import android.os.Bundle;
9import android.support.annotation.NonNull;
10import android.support.v4.app.DialogFragment;
11import android.support.v7.app.AlertDialog;
12import android.text.Editable;
13import android.text.TextUtils;
14import android.text.TextWatcher;
15import android.view.View;
16import android.widget.AdapterView;
17import android.widget.Button;
18import android.widget.Spinner;
19
20import java.security.SecureRandom;
21import java.util.ArrayList;
22import java.util.Collection;
23import java.util.List;
24
25import eu.siacs.conversations.R;
26import eu.siacs.conversations.databinding.CreateConferenceDialogBinding;
27import eu.siacs.conversations.databinding.CreatePublicChannelDialogBinding;
28import eu.siacs.conversations.entities.Account;
29import eu.siacs.conversations.services.XmppConnectionService;
30import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
31import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
32import eu.siacs.conversations.ui.util.DelayedHintHelper;
33import eu.siacs.conversations.utils.CryptoHelper;
34import eu.siacs.conversations.xmpp.XmppConnection;
35import rocks.xmpp.addr.Jid;
36
37public class CreatePublicChannelDialog extends DialogFragment implements OnBackendConnected {
38
39 private static final char[] FORBIDDEN = new char[]{'\u0022','&','\'','/',':','<','>','@'};
40
41 private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
42 private CreatePublicChannelDialogListener mListener;
43 private KnownHostsAdapter knownHostsAdapter;
44 private boolean jidWasModified = false;
45 private boolean nameEntered = false;
46 private boolean skipTetxWatcher = false;
47 private static final SecureRandom RANDOM = new SecureRandom();
48
49 public static CreatePublicChannelDialog newInstance(List<String> accounts) {
50 CreatePublicChannelDialog dialog = new CreatePublicChannelDialog();
51 Bundle bundle = new Bundle();
52 bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) accounts);
53 dialog.setArguments(bundle);
54 return dialog;
55 }
56
57 @Override
58 public void onActivityCreated(Bundle savedInstanceState) {
59 super.onActivityCreated(savedInstanceState);
60 setRetainInstance(true);
61 }
62
63 @NonNull
64 @Override
65 public Dialog onCreateDialog(Bundle savedInstanceState) {
66 jidWasModified = savedInstanceState != null && savedInstanceState.getBoolean("jid_was_modified_false", false);
67 nameEntered = savedInstanceState != null && savedInstanceState.getBoolean("name_entered", false);
68 final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
69 builder.setTitle(R.string.create_public_channel);
70 final CreatePublicChannelDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_public_channel_dialog, null, false);
71 binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
72 @Override
73 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
74 updateJidSuggestion(binding);
75 }
76
77 @Override
78 public void onNothingSelected(AdapterView<?> parent) {
79
80 }
81 });
82 binding.jid.addTextChangedListener(new TextWatcher() {
83 @Override
84 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
85
86 }
87
88 @Override
89 public void onTextChanged(CharSequence s, int start, int before, int count) {
90
91 }
92
93 @Override
94 public void afterTextChanged(Editable s) {
95 if (skipTetxWatcher) {
96 return;
97 }
98 if (jidWasModified) {
99 jidWasModified = !TextUtils.isEmpty(s);
100 } else {
101 jidWasModified = !s.toString().equals(getJidSuggestion(binding));
102 }
103 }
104 });
105 updateInputs(binding,false);
106 ArrayList<String> mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY);
107 StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account);
108 builder.setView(binding.getRoot());
109 builder.setPositiveButton(nameEntered ? R.string.create : R.string.next, null);
110 builder.setNegativeButton(nameEntered ? R.string.back : R.string.cancel, null);
111 DelayedHintHelper.setHint(R.string.channel_bare_jid_example, binding.jid);
112 this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
113 binding.jid.setAdapter(knownHostsAdapter);
114 final AlertDialog dialog = builder.create();
115 binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> {
116 submit(dialog, binding);
117 return true;
118 });
119 dialog.setOnShowListener(dialogInterface -> {
120 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> goBack(dialog, binding));
121 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> submit(dialog, binding));
122 });
123 return dialog;
124 }
125
126 private void updateJidSuggestion(CreatePublicChannelDialogBinding binding) {
127 if (jidWasModified) {
128 return;
129 }
130 String jid = getJidSuggestion(binding);
131 skipTetxWatcher = true;
132 binding.jid.setText(jid);
133 skipTetxWatcher = false;
134 }
135
136 @Override
137 public void onSaveInstanceState(Bundle outState) {
138 outState.putBoolean("jid_was_modified",jidWasModified);
139 outState.putBoolean("name_entered", nameEntered);
140 super.onSaveInstanceState(outState);
141 }
142
143 private static String getJidSuggestion(CreatePublicChannelDialogBinding binding) {
144 final Account account = StartConversationActivity.getSelectedAccount(binding.getRoot().getContext(), binding.account);
145 final XmppConnection connection = account == null ? null : account.getXmppConnection();
146 if (connection == null) {
147 return "";
148 }
149 final Editable nameText = binding.groupChatName.getText();
150 final String name = nameText == null ? "" : nameText.toString().trim();
151 final String domain = connection.getMucServer();
152 if (domain == null) {
153 return "";
154 }
155 final String localpart = clean(name);
156 if (TextUtils.isEmpty(localpart)) {
157 return "";
158 } else {
159 try {
160 return Jid.of(localpart, domain, null).toEscapedString();
161 } catch (IllegalArgumentException e) {
162 return Jid.of(CryptoHelper.pronounceable(RANDOM), domain, null).toEscapedString();
163 }
164 }
165 }
166
167 private static String clean(String name) {
168 for(char c : FORBIDDEN) {
169 name = name.replace(String.valueOf(c),"");
170 }
171 return name.replaceAll("\\s+","-");
172 }
173
174 private void goBack(AlertDialog dialog, CreatePublicChannelDialogBinding binding) {
175 if (nameEntered) {
176 nameEntered = false;
177 updateInputs(binding, true);
178 updateButtons(dialog);
179 } else {
180 dialog.dismiss();
181 }
182 }
183
184 private void submit(AlertDialog dialog, CreatePublicChannelDialogBinding binding) {
185 final Context context = binding.getRoot().getContext();
186 final Editable nameText = binding.groupChatName.getText();
187 final String name = nameText == null ? "" : nameText.toString().trim();
188 final Editable addressText = binding.jid.getText();
189 final String address = addressText == null ? "" : addressText.toString().trim();
190 if (nameEntered) {
191 binding.nameLayout.setError(null);
192 if (address.isEmpty()) {
193 binding.xmppAddressLayout.setError(context.getText(R.string.please_enter_xmpp_address));
194 } else {
195 final Jid jid;
196 try {
197 jid = Jid.ofEscaped(address);
198 } catch (IllegalArgumentException e) {
199 binding.xmppAddressLayout.setError(context.getText(R.string.invalid_jid));
200 return;
201 }
202 final Account account = StartConversationActivity.getSelectedAccount(context, binding.account);
203 if (account == null) {
204 return;
205 }
206 final XmppConnectionService service = ((XmppActivity )context).xmppConnectionService;
207 if (service != null && service.findFirstMuc(jid) != null) {
208 binding.xmppAddressLayout.setError(context.getString(R.string.channel_already_exists));
209 return;
210 }
211 mListener.onCreatePublicChannel(account, name, jid);
212 dialog.dismiss();
213 }
214 } else {
215 binding.xmppAddressLayout.setError(null);
216 if (name.isEmpty()) {
217 binding.nameLayout.setError(context.getText(R.string.please_enter_name));
218 } else if (StartConversationActivity.isValidJid(name)){
219 binding.nameLayout.setError(context.getText(R.string.this_is_an_xmpp_address));
220 } else {
221 binding.nameLayout.setError(null);
222 nameEntered = true;
223 updateInputs(binding, true);
224 updateButtons(dialog);
225 binding.jid.setText("");
226 binding.jid.append(getJidSuggestion(binding));
227 }
228 }
229 }
230
231
232 private void updateInputs(CreatePublicChannelDialogBinding binding, boolean requestFocus) {
233 binding.xmppAddressLayout.setVisibility(nameEntered ? View.VISIBLE : View.GONE);
234 binding.nameLayout.setVisibility(nameEntered ? View.GONE : View.VISIBLE);
235 if (!requestFocus) {
236 return;
237 }
238 if (nameEntered) {
239 binding.xmppAddressLayout.requestFocus();
240 } else {
241 binding.nameLayout.requestFocus();
242 }
243 }
244
245 private void updateButtons(AlertDialog dialog) {
246 final Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
247 final Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
248 positive.setText(nameEntered ? R.string.create : R.string.next);
249 negative.setText(nameEntered ? R.string.back : R.string.cancel);
250 }
251
252 @Override
253 public void onBackendConnected() {
254 refreshKnownHosts();
255 }
256
257 private void refreshKnownHosts() {
258 Activity activity = getActivity();
259 if (activity instanceof XmppActivity) {
260 Collection<String> hosts = ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
261 this.knownHostsAdapter.refresh(hosts);
262 }
263 }
264
265 public interface CreatePublicChannelDialogListener {
266 void onCreatePublicChannel(Account account, String name, Jid address);
267 }
268
269 @Override
270 public void onAttach(Context context) {
271 super.onAttach(context);
272 try {
273 mListener = (CreatePublicChannelDialogListener) context;
274 } catch (ClassCastException e) {
275 throw new ClassCastException(context.toString()
276 + " must implement CreateConferenceDialogListener");
277 }
278 }
279
280 @Override
281 public void onStart() {
282 super.onStart();
283 final Activity activity = getActivity();
284 if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) {
285 refreshKnownHosts();
286 }
287 }
288
289 @Override
290 public void onDestroyView() {
291 Dialog dialog = getDialog();
292 if (dialog != null && getRetainInstance()) {
293 dialog.setDismissMessage(null);
294 }
295 super.onDestroyView();
296 }
297}