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