1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.app.Dialog;
6import android.app.PendingIntent;
7import android.content.ActivityNotFoundException;
8import android.content.Context;
9import android.content.Intent;
10import android.content.SharedPreferences;
11import android.content.pm.PackageManager;
12import android.content.res.ColorStateList;
13import android.net.Uri;
14import android.os.Build;
15import android.os.Bundle;
16import android.preference.PreferenceManager;
17import android.text.Editable;
18import android.text.Html;
19import android.text.TextWatcher;
20import android.text.method.LinkMovementMethod;
21import android.util.Log;
22import android.util.Pair;
23import android.view.ContextMenu;
24import android.view.ContextMenu.ContextMenuInfo;
25import android.view.KeyEvent;
26import android.view.LayoutInflater;
27import android.view.Menu;
28import android.view.MenuItem;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.inputmethod.InputMethodManager;
32import android.widget.AdapterView;
33import android.widget.AdapterView.AdapterContextMenuInfo;
34import android.widget.ArrayAdapter;
35import android.widget.AutoCompleteTextView;
36import android.widget.CheckBox;
37import android.widget.EditText;
38import android.widget.ListView;
39import android.widget.Spinner;
40import android.widget.TextView;
41import android.widget.Toast;
42
43import androidx.annotation.MenuRes;
44import androidx.annotation.NonNull;
45import androidx.annotation.Nullable;
46import androidx.annotation.StringRes;
47import androidx.appcompat.app.ActionBar;
48import androidx.appcompat.app.AlertDialog;
49import androidx.appcompat.widget.PopupMenu;
50import androidx.core.content.ContextCompat;
51import androidx.databinding.DataBindingUtil;
52import androidx.fragment.app.Fragment;
53import androidx.fragment.app.FragmentManager;
54import androidx.fragment.app.FragmentTransaction;
55import androidx.recyclerview.widget.RecyclerView;
56import androidx.recyclerview.widget.LinearLayoutManager;
57import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
58import androidx.viewpager.widget.PagerAdapter;
59import androidx.viewpager.widget.ViewPager;
60
61import com.cheogram.android.FinishOnboarding;
62
63import com.google.android.material.color.MaterialColors;
64import com.google.android.material.dialog.MaterialAlertDialogBuilder;
65import com.google.android.material.textfield.TextInputLayout;
66import com.google.common.collect.Iterables;
67import com.leinardi.android.speeddial.SpeedDialActionItem;
68import com.leinardi.android.speeddial.SpeedDialView;
69
70import java.util.Arrays;
71import java.util.ArrayList;
72import java.util.Collections;
73import java.util.Comparator;
74import java.util.HashSet;
75import java.util.List;
76import java.util.Locale;
77import java.util.Map;
78import java.util.concurrent.atomic.AtomicBoolean;
79import java.util.stream.Collectors;
80
81import eu.siacs.conversations.BuildConfig;
82import eu.siacs.conversations.Config;
83import eu.siacs.conversations.R;
84import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
85import eu.siacs.conversations.entities.Account;
86import eu.siacs.conversations.entities.Bookmark;
87import eu.siacs.conversations.entities.Contact;
88import eu.siacs.conversations.entities.Conversation;
89import eu.siacs.conversations.entities.ListItem;
90import eu.siacs.conversations.entities.MucOptions;
91import eu.siacs.conversations.entities.Presence;
92import eu.siacs.conversations.services.QuickConversationsService;
93import eu.siacs.conversations.services.XmppConnectionService;
94import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
95import eu.siacs.conversations.ui.adapter.ListItemAdapter;
96import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
97import eu.siacs.conversations.ui.util.JidDialog;
98import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
99import eu.siacs.conversations.ui.util.PendingItem;
100import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
101import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
102import eu.siacs.conversations.utils.AccountUtils;
103import eu.siacs.conversations.utils.UIHelper;
104import eu.siacs.conversations.utils.XmppUri;
105import eu.siacs.conversations.utils.XEP0392Helper;
106import eu.siacs.conversations.xml.Element;
107import eu.siacs.conversations.xml.Namespace;
108import eu.siacs.conversations.xmpp.Jid;
109import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
110import eu.siacs.conversations.xmpp.XmppConnection;
111import eu.siacs.conversations.xmpp.forms.Data;
112import eu.siacs.conversations.xmpp.stanzas.IqPacket;
113
114public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
115
116 private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
117
118 public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
119
120 private final int REQUEST_SYNC_CONTACTS = 0x28cf;
121 private final int REQUEST_CREATE_CONFERENCE = 0x39da;
122 private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
123 private final PendingItem<String> mInitialSearchValue = new PendingItem<>();
124 private final AtomicBoolean oneShotKeyboardSuppress = new AtomicBoolean();
125 public ListItem contextItem;
126 private ListPagerAdapter mListPagerAdapter;
127 private final List<ListItem> contacts = new ArrayList<>();
128 private ListItemAdapter mContactsAdapter;
129 private TagsAdapter mTagsAdapter = new TagsAdapter();
130 private final List<ListItem> conferences = new ArrayList<>();
131 private ListItemAdapter mConferenceAdapter;
132 private final ArrayList<String> mActivatedAccounts = new ArrayList<>();
133 private EditText mSearchEditText;
134 private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
135 private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
136 private boolean mHideOfflineContacts = false;
137 private boolean createdByViewIntent = false;
138 private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
139
140 @Override
141 public boolean onMenuItemActionExpand(MenuItem item) {
142 mSearchEditText.post(() -> {
143 updateSearchViewHint();
144 mSearchEditText.requestFocus();
145 if (oneShotKeyboardSuppress.compareAndSet(true, false)) {
146 return;
147 }
148 InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
149 if (imm != null) {
150 imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
151 }
152 });
153 if (binding.speedDial.isOpen()) {
154 binding.speedDial.close();
155 }
156 return true;
157 }
158
159 @Override
160 public boolean onMenuItemActionCollapse(MenuItem item) {
161 SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
162 mSearchEditText.setText("");
163 filter(null);
164 navigateBack();
165 return true;
166 }
167 };
168 private final TextWatcher mSearchTextWatcher = new TextWatcher() {
169
170 @Override
171 public void afterTextChanged(Editable editable) {
172 filter(editable.toString());
173 }
174
175 @Override
176 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
177 }
178
179 @Override
180 public void onTextChanged(CharSequence s, int start, int before, int count) {
181 }
182 };
183 private MenuItem mMenuSearchView;
184 private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
185 @Override
186 public void onTagClicked(String tag) {
187 if (mMenuSearchView != null) {
188 mMenuSearchView.expandActionView();
189 mSearchEditText.setText("");
190 mSearchEditText.append(tag);
191 filter(tag);
192 }
193 }
194 };
195 private Pair<Integer, Intent> mPostponedActivityResult;
196 private Toast mToast;
197 private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
198 @Override
199 public void success(final Conversation conversation) {
200 runOnUiThread(() -> {
201 hideToast();
202 switchToConversation(conversation);
203 });
204 }
205
206 @Override
207 public void error(final int errorCode, Conversation object) {
208 runOnUiThread(() -> replaceToast(getString(errorCode)));
209 }
210
211 @Override
212 public void userInputRequired(PendingIntent pi, Conversation object) {
213
214 }
215 };
216 private ActivityStartConversationBinding binding;
217 private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
218 @Override
219 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
220 int pos = binding.startConversationViewPager.getCurrentItem();
221 if (pos == 0) {
222 if (contacts.size() == 1) {
223 openConversation(contacts.get(0));
224 return true;
225 } else if (contacts.size() == 0 && conferences.size() == 1) {
226 openConversationsForBookmark((Bookmark) conferences.get(0));
227 return true;
228 }
229 } else {
230 if (conferences.size() == 1) {
231 openConversationsForBookmark((Bookmark) conferences.get(0));
232 return true;
233 } else if (conferences.size() == 0 && contacts.size() == 1) {
234 openConversation(contacts.get(0));
235 return true;
236 }
237 }
238 SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
239 mListPagerAdapter.requestFocus(pos);
240 return true;
241 }
242 };
243
244 public static void populateAccountSpinner(final Context context, final List<String> accounts, final AutoCompleteTextView spinner) {
245 if (accounts.isEmpty()) {
246 ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
247 R.layout.item_autocomplete,
248 Collections.singletonList(context.getString(R.string.no_accounts)));
249 adapter.setDropDownViewResource(R.layout.item_autocomplete);
250 spinner.setAdapter(adapter);
251 spinner.setEnabled(false);
252 } else {
253 final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
254 adapter.setDropDownViewResource(R.layout.item_autocomplete);
255 spinner.setAdapter(adapter);
256 spinner.setEnabled(true);
257 spinner.setText(Iterables.getFirst(accounts,null),false);
258 }
259 }
260
261 public static void launch(Context context) {
262 final Intent intent = new Intent(context, StartConversationActivity.class);
263 context.startActivity(intent);
264 }
265
266 private static Intent createLauncherIntent(Context context) {
267 final Intent intent = new Intent(context, StartConversationActivity.class);
268 intent.setAction(Intent.ACTION_MAIN);
269 intent.addCategory(Intent.CATEGORY_LAUNCHER);
270 return intent;
271 }
272
273 private static boolean isViewIntent(final Intent i) {
274 return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI));
275 }
276
277 protected void hideToast() {
278 if (mToast != null) {
279 mToast.cancel();
280 }
281 }
282
283 protected void replaceToast(String msg) {
284 hideToast();
285 mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
286 mToast.show();
287 }
288
289 @Override
290 public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
291 this.refreshUi();
292 }
293
294 @Override
295 public void onCreate(Bundle savedInstanceState) {
296 super.onCreate(savedInstanceState);
297 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_start_conversation);
298 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
299 setSupportActionBar(binding.toolbar);
300 configureActionBar(getSupportActionBar());
301
302 inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
303 binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
304 binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
305 @Override
306 public void onPageSelected(int position) {
307 updateSearchViewHint();
308 }
309 });
310 mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
311 binding.startConversationViewPager.setAdapter(mListPagerAdapter);
312
313 mConferenceAdapter = new ListItemAdapter(this, conferences);
314 mContactsAdapter = new ListItemAdapter(this, contacts);
315 mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
316
317 final SharedPreferences preferences = getPreferences();
318
319 this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false);
320
321 final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching));
322
323 final Intent intent;
324 if (savedInstanceState == null) {
325 intent = getIntent();
326 } else {
327 createdByViewIntent = savedInstanceState.getBoolean("created_by_view_intent", false);
328 final String search = savedInstanceState.getString("search");
329 if (search != null) {
330 mInitialSearchValue.push(search);
331 }
332 intent = savedInstanceState.getParcelable("intent");
333 }
334
335 if (intent.getBooleanExtra("init", false)) {
336 pendingViewIntent.push(intent);
337 }
338
339 if (isViewIntent(intent)) {
340 pendingViewIntent.push(intent);
341 createdByViewIntent = true;
342 setIntent(createLauncherIntent(this));
343 } else if (startSearching && mInitialSearchValue.peek() == null) {
344 mInitialSearchValue.push("");
345 }
346 mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false));
347 mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false));
348 binding.speedDial.setOnActionSelectedListener(actionItem -> {
349 final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null;
350 final String prefilled;
351 if (isValidJid(searchString)) {
352 prefilled = Jid.ofEscaped(searchString).toEscapedString();
353 } else {
354 prefilled = null;
355 }
356 switch (actionItem.getId()) {
357 case R.id.discover_public_channels:
358 if (QuickConversationsService.isPlayStoreFlavor()) {
359 throw new IllegalStateException("Channel discovery is not available on Google Play flavor");
360 } else {
361 startActivity(new Intent(this, ChannelDiscoveryActivity.class));
362 }
363 break;
364 case R.id.create_private_group_chat:
365 showCreatePrivateGroupChatDialog();
366 break;
367 case R.id.create_public_channel:
368 showPublicChannelDialog();
369 break;
370 case R.id.create_contact:
371 showCreateContactDialog(prefilled, null);
372 break;
373 }
374 return false;
375 });
376 }
377
378 private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
379 speedDialView.clearActionItems();
380 final PopupMenu popupMenu = new PopupMenu(this, new View(this));
381 popupMenu.inflate(menuRes);
382 final Menu menu = popupMenu.getMenu();
383 for (int i = 0; i < menu.size(); i++) {
384 final MenuItem menuItem = menu.getItem(i);
385 if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) {
386 continue;
387 }
388 final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
389 .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
390 .setFabImageTintColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorOnSurface))
391 .setFabBackgroundColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorSurfaceContainerHighest))
392 .create();
393 speedDialView.addActionItem(actionItem);
394 }
395 }
396
397 public static boolean isValidJid(String input) {
398 try {
399 Jid jid = Jid.ofEscaped(input);
400 return !jid.isDomainJid();
401 } catch (IllegalArgumentException e) {
402 return false;
403 }
404 }
405
406 @Override
407 public void onSaveInstanceState(Bundle savedInstanceState) {
408 Intent pendingIntent = pendingViewIntent.peek();
409 savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
410 savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get());
411 savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
412 savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
413 if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
414 savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
415 }
416 super.onSaveInstanceState(savedInstanceState);
417 }
418
419 @Override
420 public void onStart() {
421 super.onStart();
422 if (pendingViewIntent.peek() == null) {
423 askForContactsPermissions();
424 }
425 mConferenceAdapter.refreshSettings();
426 mContactsAdapter.refreshSettings();
427 }
428
429 @Override
430 public void onNewIntent(final Intent intent) {
431 super.onNewIntent(intent);
432 if (xmppConnectionServiceBound) {
433 processViewIntent(intent);
434 } else {
435 pendingViewIntent.push(intent);
436 }
437 setIntent(createLauncherIntent(this));
438 }
439
440 protected void openConversationForContact(int position) {
441 openConversation(contacts.get(position));
442 }
443
444 protected void openConversation(ListItem item) {
445 if (item instanceof Contact) {
446 openConversationForContact((Contact) item);
447 } else {
448 openConversationsForBookmark((Bookmark) item);
449 }
450 }
451
452 protected void openConversationForContact(Contact contact) {
453 Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
454 SoftKeyboardUtils.hideSoftKeyboard(this);
455 switchToConversation(conversation);
456 }
457
458 protected void openConversationForBookmark(int position) {
459 Bookmark bookmark = (Bookmark) conferences.get(position);
460 openConversationsForBookmark(bookmark);
461 }
462
463 protected void shareBookmarkUri() {
464 shareAsChannel(this, contextItem.getJid().asBareJid().toEscapedString());
465 }
466
467 protected void shareBookmarkUri(int position) {
468 Bookmark bookmark = (Bookmark) conferences.get(position);
469 shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString());
470 }
471
472 public static void shareAsChannel(final Context context, final String address) {
473 Intent shareIntent = new Intent();
474 shareIntent.setAction(Intent.ACTION_SEND);
475 shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + Uri.encode(address, "@/+") + "?join");
476 shareIntent.setType("text/plain");
477 try {
478 context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
479 } catch (ActivityNotFoundException e) {
480 Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
481 }
482 }
483
484 protected void openConversationsForBookmark(final Bookmark bookmark) {
485 final Jid jid = bookmark.getFullJid();
486 if (jid == null) {
487 Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
488 return;
489 }
490 final Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
491 bookmark.setConversation(conversation);
492 if (!bookmark.autojoin()) {
493 bookmark.setAutojoin(true);
494 xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
495 }
496 SoftKeyboardUtils.hideSoftKeyboard(this);
497 switchToConversation(conversation);
498 }
499
500 protected void openDetailsForContact() {
501 switchToContactDetails((Contact) contextItem);
502 }
503
504 protected void showQrForContact() {
505 showQrCode("xmpp:" + contextItem.getJid().asBareJid().toEscapedString());
506 }
507
508 protected void toggleContactBlock() {
509 BlockContactDialog.show(this, (Contact) contextItem);
510 }
511
512 protected void deleteContact() {
513 final Contact contact = (Contact) contextItem;
514 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
515 builder.setNegativeButton(R.string.cancel, null);
516 builder.setTitle(R.string.action_delete_contact);
517 builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
518 builder.setPositiveButton(R.string.delete, (dialog, which) -> {
519 xmppConnectionService.deleteContactOnServer(contact);
520 filter(mSearchEditText.getText().toString());
521 });
522 builder.create().show();
523 }
524
525 protected void deleteConference() {
526 final Bookmark bookmark = (Bookmark) contextItem;
527 final var conversation = bookmark.getConversation();
528 final boolean hasConversation = conversation != null;
529 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
530 builder.setNegativeButton(R.string.cancel, null);
531 builder.setTitle(R.string.delete_bookmark);
532 if (hasConversation) {
533 builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString()));
534 } else {
535 builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
536 }
537 builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> {
538 bookmark.setConversation(null);
539 final Account account = bookmark.getAccount();
540 xmppConnectionService.deleteBookmark(account, bookmark);
541 if (conversation != null) {
542 xmppConnectionService.archiveConversation(conversation);
543 }
544 filter(mSearchEditText.getText().toString());
545 });
546 builder.create().show();
547
548 }
549
550 @SuppressLint("InflateParams")
551 protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
552 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
553 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
554 if (prev != null) {
555 ft.remove(prev);
556 }
557 ft.addToBackStack(null);
558 EnterJidDialog dialog = EnterJidDialog.newInstance(
559 mActivatedAccounts,
560 "Start Conversation",
561 getString(R.string.message),
562 "Call",
563 prefilledJid,
564 invite == null ? null : invite.account,
565 invite == null || !invite.hasFingerprints(),
566 true,
567 EnterJidDialog.SanityCheck.ALLOW_MUC
568 );
569
570 dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, call, save) -> {
571 if (!xmppConnectionServiceBound) {
572 return false;
573 }
574
575 final Account account = xmppConnectionService.findAccountByJid(accountJid);
576 if (account == null) {
577 return true;
578 }
579 final Contact contact = account.getRoster().getContact(contactJid);
580
581 if (invite != null && invite.getName() != null) {
582 contact.setServerName(invite.getName());
583 }
584
585 if (contact.isSelf() || contact.showInRoster()) {
586 switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
587 return true;
588 }
589
590 xmppConnectionService.checkIfMuc(account, contactJid, (isMuc) -> {
591 runOnUiThread(() -> {
592 if (isMuc) {
593 if (save) {
594 Bookmark bookmark = account.getBookmark(contactJid);
595 if (bookmark != null) {
596 openConversationsForBookmark(bookmark);
597 } else {
598 bookmark = new Bookmark(account, contactJid.asBareJid());
599 bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
600 final String nick = contactJid.getResource();
601 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
602 bookmark.setNick(nick);
603 }
604 xmppConnectionService.createBookmark(account, bookmark);
605 final Conversation conversation = xmppConnectionService
606 .findOrCreateConversation(account, contactJid, true, true, true);
607 bookmark.setConversation(conversation);
608 switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
609 }
610 } else {
611 final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, true, true, true);
612 switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
613 }
614 } else {
615 if (save) {
616 final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
617 xmppConnectionService.createContact(contact, true, preAuth);
618 if (invite != null && invite.hasFingerprints()) {
619 xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
620 }
621 }
622 switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
623 }
624
625 try {
626 dialog.dismiss();
627 } catch (final IllegalStateException e) { }
628 });
629 });
630
631 return false;
632 });
633 dialog.show(ft, FRAGMENT_TAG_DIALOG);
634 }
635
636 @SuppressLint("InflateParams")
637 protected void showJoinConferenceDialog(final String prefilledJid, final Invite invite) {
638 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
639 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
640 if (prev != null) {
641 ft.remove(prev);
642 }
643 ft.addToBackStack(null);
644 JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts);
645 joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
646 }
647
648 private void showCreatePrivateGroupChatDialog() {
649 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
650 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
651 if (prev != null) {
652 ft.remove(prev);
653 }
654 ft.addToBackStack(null);
655 CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
656 createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
657 }
658
659 private void showPublicChannelDialog() {
660 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
661 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
662 if (prev != null) {
663 ft.remove(prev);
664 }
665 ft.addToBackStack(null);
666 CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts);
667 dialog.show(ft, FRAGMENT_TAG_DIALOG);
668 }
669
670 public static Account getSelectedAccount(final Context context, final AutoCompleteTextView spinner) {
671 if (spinner == null || !spinner.isEnabled()) {
672 return null;
673 }
674 if (context instanceof XmppActivity) {
675 final Jid jid;
676 try {
677 jid = Jid.ofEscaped(spinner.getText().toString());
678 } catch (final IllegalArgumentException e) {
679 return null;
680 }
681 final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService;
682 if (service == null) {
683 return null;
684 }
685 return service.findAccountByJid(jid);
686 } else {
687 return null;
688 }
689 }
690
691 protected void switchToConversation(Contact contact) {
692 Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
693 switchToConversation(conversation);
694 }
695
696 protected void switchToConversationDoNotAppend(Contact contact, String body) {
697 switchToConversationDoNotAppend(contact, body, null);
698 }
699
700 protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
701 Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
702 switchToConversation(conversation, body, false, null, false, true, postInit);
703 }
704
705 @Override
706 public void invalidateOptionsMenu() {
707 boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded();
708 String text = mSearchEditText != null ? mSearchEditText.getText().toString() : "";
709 if (isExpanded) {
710 mInitialSearchValue.push(text);
711 oneShotKeyboardSuppress.set(true);
712 }
713 super.invalidateOptionsMenu();
714 }
715
716 private void updateSearchViewHint() {
717 if (binding == null || mSearchEditText == null) {
718 return;
719 }
720 if (binding.startConversationViewPager.getCurrentItem() == 0) {
721 mSearchEditText.setHint("Search chats");
722 mSearchEditText.setContentDescription("Search chats");
723 } else {
724 mSearchEditText.setHint(R.string.search_group_chats);
725 mSearchEditText.setContentDescription(getString(R.string.search_group_chats));
726 }
727 }
728
729 @Override
730 public boolean onCreateOptionsMenu(final Menu menu) {
731 getMenuInflater().inflate(R.menu.start_conversation, menu);
732 AccountUtils.showHideMenuItems(menu);
733 final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
734 final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
735 final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
736 privacyPolicyMenuItem.setVisible(
737 BuildConfig.PRIVACY_POLICY != null
738 && QuickConversationsService.isPlayStoreFlavor());
739 qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
740 if (QuickConversationsService.isQuicksy()) {
741 menuHideOffline.setVisible(false);
742 } else {
743 menuHideOffline.setVisible(true);
744 menuHideOffline.setChecked(this.mHideOfflineContacts);
745 }
746 mMenuSearchView = menu.findItem(R.id.action_search);
747 mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
748 View mSearchView = mMenuSearchView.getActionView();
749 mSearchEditText = mSearchView.findViewById(R.id.search_field);
750 mSearchEditText.addTextChangedListener(mSearchTextWatcher);
751 mSearchEditText.setOnEditorActionListener(mSearchDone);
752
753 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
754 boolean showDynamicTags = preferences.getBoolean("show_dynamic_tags", getResources().getBoolean(R.bool.show_dynamic_tags));
755 if (showDynamicTags) {
756 RecyclerView tags = mSearchView.findViewById(R.id.tags);
757 androidx.recyclerview.widget.DividerItemDecoration spacer = new androidx.recyclerview.widget.DividerItemDecoration(tags.getContext(), LinearLayoutManager.HORIZONTAL);
758 spacer.setDrawable(getResources().getDrawable(R.drawable.horizontal_space));
759 tags.addItemDecoration(spacer);
760 tags.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
761 tags.setAdapter(mTagsAdapter);
762 }
763
764 String initialSearchValue = mInitialSearchValue.pop();
765 if (initialSearchValue != null) {
766 mMenuSearchView.expandActionView();
767 try {
768 mSearchEditText.append(initialSearchValue);
769 } catch (final StringIndexOutOfBoundsException e) {
770 mSearchEditText.setText(initialSearchValue);
771 }
772 filter(initialSearchValue);
773 }
774 updateSearchViewHint();
775 return super.onCreateOptionsMenu(menu);
776 }
777
778 @Override
779 public boolean onOptionsItemSelected(MenuItem item) {
780 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
781 return false;
782 }
783 switch (item.getItemId()) {
784 case android.R.id.home:
785 navigateBack();
786 return true;
787 case R.id.action_scan_qr_code:
788 UriHandlerActivity.scan(this);
789 return true;
790 case R.id.action_hide_offline:
791 mHideOfflineContacts = !item.isChecked();
792 getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).apply();
793 if (mSearchEditText != null) {
794 filter(mSearchEditText.getText().toString());
795 }
796 invalidateOptionsMenu();
797 }
798 return super.onOptionsItemSelected(item);
799 }
800
801 @Override
802 public boolean onKeyUp(int keyCode, KeyEvent event) {
803 if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
804 openSearch();
805 return true;
806 }
807 int c = event.getUnicodeChar();
808 if (c > 32) {
809 if (mSearchEditText != null && !mSearchEditText.isFocused()) {
810 openSearch();
811 mSearchEditText.append(Character.toString((char) c));
812 return true;
813 }
814 }
815 return super.onKeyUp(keyCode, event);
816 }
817
818 private void openSearch() {
819 if (mMenuSearchView != null) {
820 mMenuSearchView.expandActionView();
821 }
822 }
823
824 @Override
825 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
826 if (resultCode == RESULT_OK) {
827 if (xmppConnectionServiceBound) {
828 this.mPostponedActivityResult = null;
829 if (requestCode == REQUEST_CREATE_CONFERENCE) {
830 Account account = extractAccount(intent);
831 final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
832 final List<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
833 if (account != null && jids.size() > 0) {
834 // This hardcodes cheogram.com and is in general a terrible hack
835 // Ideally this would be based around XEP-0033 but until we think of a good fallback behaviour we keep using this gross commas thing
836 if (jids.stream().allMatch(jid -> jid.getDomain().toString().equals("cheogram.com"))) {
837 new AlertDialog.Builder(this)
838 .setMessage("You appear to be creating a group with only SMS contacts. Would you like to create a channel or an MMS group text?")
839 .setNeutralButton("Channel", (d, w) -> {
840 if (xmppConnectionService.createAdhocConference(account, name, jids, mAdhocConferenceCallback)) {
841 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
842 mToast.show();
843 }
844 }).setPositiveButton("Group Text", (d, w) -> {
845 Jid groupJid = Jid.ofLocalAndDomain(jids.stream().map(jid -> jid.getLocal()).sorted().collect(Collectors.joining(",")), "cheogram.com");
846 Contact group = account.getRoster().getContact(groupJid);
847 if (name != null && !name.equals("")) group.setServerName(name);
848 xmppConnectionService.createContact(group, true);
849 switchToConversation(group);
850 }).create().show();
851 } else {
852 if (xmppConnectionService.createAdhocConference(account, name, jids, mAdhocConferenceCallback)) {
853 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
854 mToast.show();
855 }
856 }
857 }
858 }
859 } else {
860 this.mPostponedActivityResult = new Pair<>(requestCode, intent);
861 }
862 }
863 super.onActivityResult(requestCode, requestCode, intent);
864 }
865
866 private void askForContactsPermissions() {
867 if (QuickConversationsService.isContactListIntegration(this)) {
868 if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
869 != PackageManager.PERMISSION_GRANTED) {
870 if (mRequestedContactsPermission.compareAndSet(false, true)) {
871 final String consent =
872 PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
873 .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
874 final boolean requiresConsent =
875 (QuickConversationsService.isQuicksy()
876 || QuickConversationsService.isPlayStoreFlavor())
877 && !"agreed".equals(consent);
878 if (requiresConsent && "declined".equals(consent)) {
879 Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
880 return;
881 }
882 if (requiresConsent
883 || shouldShowRequestPermissionRationale(
884 Manifest.permission.READ_CONTACTS)) {
885 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
886 final AtomicBoolean requestPermission = new AtomicBoolean(false);
887 if (QuickConversationsService.isQuicksy()) {
888 builder.setTitle(R.string.quicksy_wants_your_consent);
889 builder.setMessage(
890 Html.fromHtml(
891 getString(R.string.sync_with_contacts_quicksy_static)));
892 } else {
893 builder.setTitle(R.string.sync_with_contacts);
894 builder.setMessage(
895 getString(
896 R.string.sync_with_contacts_long,
897 getString(R.string.app_name)));
898 }
899 @StringRes int confirmButtonText;
900 if (requiresConsent) {
901 confirmButtonText = R.string.agree_and_continue;
902 } else {
903 confirmButtonText = R.string.next;
904 }
905 builder.setPositiveButton(
906 confirmButtonText,
907 (dialog, which) -> {
908 if (requiresConsent) {
909 PreferenceManager.getDefaultSharedPreferences(
910 getApplicationContext())
911 .edit()
912 .putString(
913 PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
914 .apply();
915 }
916 if (requestPermission.compareAndSet(false, true)) {
917 requestPermissions(
918 new String[] {Manifest.permission.READ_CONTACTS},
919 REQUEST_SYNC_CONTACTS);
920 }
921 });
922 if (requiresConsent) {
923 builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
924 getApplicationContext())
925 .edit()
926 .putString(
927 PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
928 .apply());
929 } else {
930 builder.setOnDismissListener(
931 dialog -> {
932 if (requestPermission.compareAndSet(false, true)) {
933 requestPermissions(
934 new String[] {
935 Manifest.permission.READ_CONTACTS
936 },
937 REQUEST_SYNC_CONTACTS);
938 }
939 });
940 }
941 builder.setCancelable(requiresConsent);
942 final AlertDialog dialog = builder.create();
943 dialog.setCanceledOnTouchOutside(requiresConsent);
944 dialog.setOnShowListener(
945 dialogInterface -> {
946 final TextView tv = dialog.findViewById(android.R.id.message);
947 if (tv != null) {
948 tv.setMovementMethod(LinkMovementMethod.getInstance());
949 }
950 });
951 dialog.show();
952 } else {
953 requestPermissions(
954 new String[] {Manifest.permission.READ_CONTACTS},
955 REQUEST_SYNC_CONTACTS);
956 }
957 }
958 }
959 }
960 }
961
962 @Override
963 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
964 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
965 if (grantResults.length > 0)
966 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
967 ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
968 if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
969 if (QuickConversationsService.isQuicksy()) {
970 setRefreshing(true);
971 }
972 xmppConnectionService.loadPhoneContacts();
973 xmppConnectionService.startContactObserver();
974 }
975 }
976 }
977
978 private void configureHomeButton() {
979 final ActionBar actionBar = getSupportActionBar();
980 if (actionBar == null) {
981 return;
982 }
983 boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
984 actionBar.setDisplayHomeAsUpEnabled(openConversations);
985 actionBar.setDisplayHomeAsUpEnabled(openConversations);
986
987 }
988
989 @Override
990 protected void onBackendConnected() {
991 if (QuickConversationsService.isContactListIntegration(this)
992 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
993 || checkSelfPermission(Manifest.permission.READ_CONTACTS)
994 == PackageManager.PERMISSION_GRANTED)) {
995 xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
996 }
997 if (mPostponedActivityResult != null) {
998 onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
999 this.mPostponedActivityResult = null;
1000 }
1001 this.mActivatedAccounts.clear();
1002 this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService));
1003 configureHomeButton();
1004 Intent intent = pendingViewIntent.pop();
1005
1006 final boolean onboardingCancel = xmppConnectionService.getPreferences().getString("onboarding_action", "").equals("cancel");
1007 if (onboardingCancel) xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
1008
1009 if ((xmppConnectionService.isOnboarding() || (intent != null && intent.getBooleanExtra("init", false))) && !onboardingCancel && !xmppConnectionService.getAccounts().isEmpty()) {
1010 Account selectedAccount = xmppConnectionService.getAccounts().get(0);
1011 final String accountJid = intent == null ? null : intent.getStringExtra(EXTRA_ACCOUNT);
1012 intent = null;
1013 boolean hasPstnOrSms = false;
1014 Account onboardingAccount = null;
1015 outer:
1016 for (Account account : xmppConnectionService.getAccounts()) {
1017 if (onboardingAccount == null && account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) onboardingAccount = account;
1018
1019 if (accountJid != null) {
1020 if(account.getJid().asBareJid().toEscapedString().equals(accountJid)) {
1021 selectedAccount = account;
1022 } else {
1023 continue;
1024 }
1025 }
1026
1027 for (Contact contact : account.getRoster().getContacts()) {
1028 if (contact.getPresences().anyIdentity("gateway", "pstn")) {
1029 hasPstnOrSms = true;
1030 break outer;
1031 }
1032 if (contact.getPresences().anyIdentity("gateway", "sms")) {
1033 hasPstnOrSms = true;
1034 break outer;
1035 }
1036 }
1037 }
1038
1039 if (!hasPstnOrSms) {
1040 if (onboardingAccount != null && !selectedAccount.getJid().equals(onboardingAccount.getJid())) {
1041 FinishOnboarding.finish(xmppConnectionService, this, onboardingAccount, selectedAccount);
1042 } else {
1043 startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register");
1044 finish();
1045 return;
1046 }
1047 }
1048 }
1049
1050 if (intent != null && processViewIntent(intent)) {
1051 filter(null);
1052 } else {
1053 if (mSearchEditText != null) {
1054 filter(mSearchEditText.getText().toString());
1055 } else {
1056 filter(null);
1057 }
1058 }
1059 Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
1060 if (fragment instanceof OnBackendConnected) {
1061 Log.d(Config.LOGTAG, "calling on backend connected on dialog");
1062 ((OnBackendConnected) fragment).onBackendConnected();
1063 }
1064 if (QuickConversationsService.isQuicksy()) {
1065 setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1066 }
1067 if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) {
1068 binding.speedDial.open();
1069 }
1070 }
1071
1072 protected boolean processViewIntent(@NonNull Intent intent) {
1073 final String inviteUri = intent.getStringExtra(EXTRA_INVITE_URI);
1074 if (inviteUri != null) {
1075 final Invite invite = new Invite(inviteUri);
1076 invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1077 if (invite.isValidJid()) {
1078 return invite.invite();
1079 }
1080 }
1081 final String action = intent.getAction();
1082 if (action == null) {
1083 return false;
1084 }
1085 switch (action) {
1086 case Intent.ACTION_SENDTO:
1087 case Intent.ACTION_VIEW:
1088 Uri uri = intent.getData();
1089 if (uri != null) {
1090 Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
1091 invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1092 invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
1093 return invite.invite();
1094 } else {
1095 return false;
1096 }
1097 }
1098 return false;
1099 }
1100
1101 private boolean handleJid(Invite invite) {
1102 final List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
1103 final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account);
1104 if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) {
1105 if (muc != null && !invite.forceDialog) {
1106 if (invite.getParameter("password") != null) {
1107 xmppConnectionService.providePasswordForMuc(muc, invite.getParameter("password"));
1108 }
1109 switchToConversationDoNotAppend(muc, invite.getBody());
1110 return true;
1111 } else {
1112 showJoinConferenceDialog(invite.getJid().asBareJid().toEscapedString(), invite);
1113 return false;
1114 }
1115 } else if (contacts.size() == 0) {
1116 showCreateContactDialog(invite.getJid().toEscapedString(), invite);
1117 return false;
1118 } else if (contacts.size() == 1) {
1119 Contact contact = contacts.get(0);
1120 if (!invite.isSafeSource() && invite.hasFingerprints()) {
1121 displayVerificationWarningDialog(contact, invite);
1122 } else {
1123 if (invite.hasFingerprints()) {
1124 if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
1125 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
1126 }
1127 }
1128 if (invite.account != null) {
1129 xmppConnectionService.getShortcutService().report(contact);
1130 }
1131 switchToConversationDoNotAppend(contact, invite.getBody());
1132 }
1133 return true;
1134 } else {
1135 if (mMenuSearchView != null) {
1136 mMenuSearchView.expandActionView();
1137 mSearchEditText.setText("");
1138 mSearchEditText.append(invite.getJid().toEscapedString());
1139 filter(invite.getJid().toEscapedString());
1140 } else {
1141 mInitialSearchValue.push(invite.getJid().toEscapedString());
1142 }
1143 return true;
1144 }
1145 }
1146
1147 private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
1148 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1149 builder.setTitle(R.string.verify_omemo_keys);
1150 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
1151 final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
1152 TextView warning = view.findViewById(R.id.warning);
1153 warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName()));
1154 builder.setView(view);
1155 builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
1156 if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
1157 xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
1158 }
1159 switchToConversationDoNotAppend(contact, invite.getBody());
1160 });
1161 builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
1162 AlertDialog dialog = builder.create();
1163 dialog.setCanceledOnTouchOutside(false);
1164 dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
1165 dialog.show();
1166 }
1167
1168 protected void filter(String needle) {
1169 if (xmppConnectionServiceBound) {
1170 this.filterContacts(needle);
1171 this.filterConferences(needle);
1172 }
1173 }
1174
1175 protected void filterContacts(String needle) {
1176 this.contacts.clear();
1177 ArrayList<ListItem.Tag> tags = new ArrayList<>();
1178 final List<Account> accounts = xmppConnectionService.getAccounts();
1179 boolean foundSopranica = false;
1180 for (final Account account : accounts) {
1181 if (account.isEnabled()) {
1182 for (Contact contact : account.getRoster().getContacts()) {
1183 Presence.Status s = contact.getShownStatus();
1184 if (contact.showInContactList() && contact.match(this, needle)
1185 && (!this.mHideOfflineContacts
1186 || (needle != null && !needle.trim().isEmpty())
1187 || s.compareTo(Presence.Status.OFFLINE) < 0)) {
1188 this.contacts.add(contact);
1189 tags.addAll(contact.getTags(this));
1190 }
1191 }
1192
1193 final Contact self = new Contact(account.getSelfContact());
1194 self.setSystemName("Note to Self");
1195 if (self.match(this, needle)) {
1196 this.contacts.add(self);
1197 }
1198
1199 for (Bookmark bookmark : account.getBookmarks()) {
1200 if (bookmark.match(this, needle)) {
1201 if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
1202 foundSopranica = true;
1203 }
1204 this.contacts.add(bookmark);
1205 tags.addAll(bookmark.getTags(this));
1206 }
1207 }
1208 }
1209 }
1210
1211 Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
1212 sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
1213
1214 mTagsAdapter.setTags(
1215 tags.stream()
1216 .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
1217 .entrySet().stream()
1218 .sorted(sortTagsBy)
1219 .map(e -> e.getKey()).collect(Collectors.toList())
1220 );
1221 Collections.sort(this.contacts);
1222
1223 final boolean sopranicaDeleted = getPreferences().getBoolean("cheogram_sopranica_bookmark_deleted", false);
1224
1225 if (!sopranicaDeleted && !foundSopranica && (needle == null || needle.equals("")) && xmppConnectionService.getAccounts().size() > 0) {
1226 Bookmark bookmark = new Bookmark(
1227 xmppConnectionService.getAccounts().get(0),
1228 Jid.of("discuss@conference.soprani.ca")
1229 );
1230 bookmark.setBookmarkName("Soprani.ca / Cheogram Discussion");
1231 bookmark.addChild("group").setContent("support");
1232 this.contacts.add(0, bookmark);
1233 }
1234
1235 mContactsAdapter.notifyDataSetChanged();
1236 }
1237
1238 protected void filterConferences(String needle) {
1239 this.conferences.clear();
1240 for (final Account account : xmppConnectionService.getAccounts()) {
1241 if (account.isEnabled()) {
1242 for (final Bookmark bookmark : account.getBookmarks()) {
1243 if (bookmark.match(this, needle)) {
1244 this.conferences.add(bookmark);
1245 }
1246 }
1247 }
1248 }
1249 Collections.sort(this.conferences);
1250 mConferenceAdapter.notifyDataSetChanged();
1251 }
1252
1253 @Override
1254 public void OnUpdateBlocklist(final Status status) {
1255 refreshUi();
1256 }
1257
1258 @Override
1259 protected void refreshUiReal() {
1260 if (mSearchEditText != null) {
1261 filter(mSearchEditText.getText().toString());
1262 }
1263 configureHomeButton();
1264 if (QuickConversationsService.isQuicksy()) {
1265 setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1266 }
1267 }
1268
1269 @Override
1270 public void onBackPressed() {
1271 if (binding.speedDial.isOpen()) {
1272 binding.speedDial.close();
1273 return;
1274 }
1275 navigateBack();
1276 }
1277
1278 private void navigateBack() {
1279 if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) {
1280 Intent intent = new Intent(this, ConversationsActivity.class);
1281 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
1282 startActivity(intent);
1283 }
1284 finish();
1285 }
1286
1287 @Override
1288 public void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String name) {
1289 if (!xmppConnectionServiceBound) {
1290 return;
1291 }
1292 final Account account = getSelectedAccount(this, spinner);
1293 if (account == null) {
1294 return;
1295 }
1296 Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
1297 intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false);
1298 intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true);
1299 intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim());
1300 intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
1301 intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
1302 startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
1303 }
1304
1305 @Override
1306 public void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout layout, AutoCompleteTextView jid, String password) {
1307 if (!xmppConnectionServiceBound) {
1308 return;
1309 }
1310 final Account account = getSelectedAccount(this, spinner);
1311 if (account == null) {
1312 return;
1313 }
1314 final String input = jid.getText().toString().trim();
1315 Jid conferenceJid;
1316 try {
1317 conferenceJid = Jid.ofEscaped(input);
1318 } catch (final IllegalArgumentException e) {
1319 final XmppUri xmppUri = new XmppUri(input);
1320 if (xmppUri.isValidJid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) {
1321 final Editable editable = jid.getEditableText();
1322 editable.clear();
1323 editable.append(xmppUri.getJid().toEscapedString());
1324 conferenceJid = xmppUri.getJid();
1325 } else {
1326 layout.setError(getString(R.string.invalid_jid));
1327 return;
1328 }
1329 }
1330 final var existingBookmark = account.getBookmark(conferenceJid);
1331 if (existingBookmark != null) {
1332 openConversationsForBookmark(existingBookmark);
1333 } else {
1334 final var bookmark = new Bookmark(account, conferenceJid.asBareJid());
1335 bookmark.setAutojoin(true);
1336 final String nick = conferenceJid.getResource();
1337 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
1338 bookmark.setNick(nick);
1339 }
1340 xmppConnectionService.createBookmark(account, bookmark);
1341 final Conversation conversation = xmppConnectionService
1342 .findOrCreateConversation(account, conferenceJid, true, true, null, true, password);
1343
1344 bookmark.setConversation(conversation);
1345 switchToConversation(conversation);
1346 }
1347 dialog.dismiss();
1348 }
1349
1350 @Override
1351 public void onConversationUpdate() {
1352 refreshUi();
1353 }
1354
1355 @Override
1356 public void onRefresh() {
1357 Log.d(Config.LOGTAG, "user requested to refresh");
1358 if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) {
1359 xmppConnectionService.getQuickConversationsService().considerSyncBackground(true);
1360 }
1361 }
1362
1363
1364 private void setRefreshing(boolean refreshing) {
1365 MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0);
1366 if (fragment != null) {
1367 fragment.setRefreshing(refreshing);
1368 }
1369 }
1370
1371 @Override
1372 public void onCreatePublicChannel(Account account, String name, Jid address) {
1373 mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG);
1374 mToast.show();
1375 xmppConnectionService.createPublicChannel(account, name, address, new UiCallback<Conversation>() {
1376 @Override
1377 public void success(Conversation conversation) {
1378 runOnUiThread(() -> {
1379 hideToast();
1380 switchToConversation(conversation);
1381 });
1382
1383 }
1384
1385 @Override
1386 public void error(int errorCode, Conversation conversation) {
1387 runOnUiThread(() -> {
1388 replaceToast(getString(errorCode));
1389 switchToConversation(conversation);
1390 });
1391 }
1392
1393 @Override
1394 public void userInputRequired(PendingIntent pi, Conversation object) {
1395
1396 }
1397 });
1398 }
1399
1400 public static class MyListFragment extends SwipeRefreshListFragment {
1401 private AdapterView.OnItemClickListener mOnItemClickListener;
1402 private int mResContextMenu;
1403
1404 public void setContextMenu(final int res) {
1405 this.mResContextMenu = res;
1406 }
1407
1408 @Override
1409 public void onListItemClick(final ListView l, final View v, final int position, final long id) {
1410 if (mOnItemClickListener != null) {
1411 mOnItemClickListener.onItemClick(l, v, position, id);
1412 }
1413 }
1414
1415 public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1416 this.mOnItemClickListener = l;
1417 }
1418
1419 @Override
1420 public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1421 super.onViewCreated(view, savedInstanceState);
1422 registerForContextMenu(getListView());
1423 getListView().setFastScrollEnabled(true);
1424 getListView().setDivider(null);
1425 getListView().setDividerHeight(0);
1426 }
1427
1428 @Override
1429 public void onCreateContextMenu(@NonNull final ContextMenu menu, @NonNull final View v, final ContextMenuInfo menuInfo) {
1430 super.onCreateContextMenu(menu, v, menuInfo);
1431 final StartConversationActivity activity = (StartConversationActivity) getActivity();
1432 if (activity == null) {
1433 return;
1434 }
1435 final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1436 activity.contextItem = null;
1437 if (mResContextMenu == R.menu.contact_context) {
1438 activity.contextItem = activity.contacts.get(acmi.position);
1439 } else if (mResContextMenu == R.menu.conference_context) {
1440 activity.contextItem = activity.conferences.get(acmi.position);
1441 }
1442 if (activity.contextItem instanceof Bookmark) {
1443 activity.getMenuInflater().inflate(R.menu.conference_context, menu);
1444 final Bookmark bookmark = (Bookmark) activity.contextItem;
1445 final Conversation conversation = bookmark.getConversation();
1446 final MenuItem share = menu.findItem(R.id.context_share_uri);
1447 final MenuItem delete = menu.findItem(R.id.context_delete_conference);
1448 if (conversation != null) {
1449 delete.setTitle(R.string.delete_and_close);
1450 } else {
1451 delete.setTitle(R.string.delete_bookmark);
1452 }
1453 share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous());
1454 } else if (activity.contextItem instanceof Contact) {
1455 activity.getMenuInflater().inflate(R.menu.contact_context, menu);
1456 final Contact contact = (Contact) activity.contextItem;
1457 final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1458 final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1459 final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact);
1460 if (contact.isSelf()) {
1461 showContactDetailsItem.setVisible(false);
1462 }
1463 deleteContactMenuItem.setVisible(contact.showInRoster() && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER));
1464 final XmppConnection xmpp = contact.getAccount().getXmppConnection();
1465 if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1466 if (contact.isBlocked()) {
1467 blockUnblockItem.setTitle(R.string.unblock_contact);
1468 } else {
1469 blockUnblockItem.setTitle(R.string.block_contact);
1470 }
1471 } else {
1472 blockUnblockItem.setVisible(false);
1473 }
1474 }
1475 }
1476
1477 @Override
1478 public boolean onContextItemSelected(final MenuItem item) {
1479 StartConversationActivity activity = (StartConversationActivity) getActivity();
1480 if (activity == null) {
1481 return true;
1482 }
1483 switch (item.getItemId()) {
1484 case R.id.context_contact_details:
1485 activity.openDetailsForContact();
1486 break;
1487 case R.id.context_show_qr:
1488 activity.showQrForContact();
1489 break;
1490 case R.id.context_contact_block_unblock:
1491 activity.toggleContactBlock();
1492 break;
1493 case R.id.context_delete_contact:
1494 activity.deleteContact();
1495 break;
1496 case R.id.context_share_uri:
1497 activity.shareBookmarkUri();
1498 break;
1499 case R.id.context_delete_conference:
1500 activity.deleteConference();
1501 }
1502 return true;
1503 }
1504 }
1505
1506 public class ListPagerAdapter extends PagerAdapter {
1507 private final FragmentManager fragmentManager;
1508 private final MyListFragment[] fragments;
1509
1510 ListPagerAdapter(FragmentManager fm) {
1511 fragmentManager = fm;
1512 fragments = new MyListFragment[2];
1513 }
1514
1515 public void requestFocus(int pos) {
1516 if (fragments.length > pos) {
1517 fragments[pos].getListView().requestFocus();
1518 }
1519 }
1520
1521 @Override
1522 public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
1523 FragmentTransaction trans = fragmentManager.beginTransaction();
1524 trans.remove(fragments[position]);
1525 trans.commit();
1526 fragments[position] = null;
1527 }
1528
1529 @NonNull
1530 @Override
1531 public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
1532 final Fragment fragment = getItem(position);
1533 final FragmentTransaction trans = fragmentManager.beginTransaction();
1534 trans.add(container.getId(), fragment, "fragment:" + position);
1535 try {
1536 trans.commit();
1537 } catch (IllegalStateException e) {
1538 //ignore
1539 }
1540 return fragment;
1541 }
1542
1543 @Override
1544 public int getCount() {
1545 return fragments.length;
1546 }
1547
1548 @Override
1549 public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) {
1550 return ((Fragment) fragment).getView() == view;
1551 }
1552
1553 @Nullable
1554 @Override
1555 public CharSequence getPageTitle(int position) {
1556 switch (position) {
1557 case 0:
1558 return getResources().getString(R.string.contacts);
1559 case 1:
1560 return getResources().getString(R.string.group_chats);
1561 default:
1562 return super.getPageTitle(position);
1563 }
1564 }
1565
1566 Fragment getItem(int position) {
1567 if (fragments[position] == null) {
1568 final MyListFragment listFragment = new MyListFragment();
1569 if (position == 1) {
1570 listFragment.setListAdapter(mConferenceAdapter);
1571 listFragment.setContextMenu(R.menu.conference_context);
1572 listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1573 } else {
1574 listFragment.setListAdapter(mContactsAdapter);
1575 listFragment.setContextMenu(R.menu.contact_context);
1576 listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p));
1577 if (QuickConversationsService.isQuicksy()) {
1578 listFragment.setOnRefreshListener(StartConversationActivity.this);
1579 }
1580 }
1581 fragments[position] = listFragment;
1582 }
1583 return fragments[position];
1584 }
1585 }
1586
1587 public static void addInviteUri(Intent to, Intent from) {
1588 if (from != null && from.hasExtra(EXTRA_INVITE_URI)) {
1589 final String invite = from.getStringExtra(EXTRA_INVITE_URI);
1590 to.putExtra(EXTRA_INVITE_URI, invite);
1591 }
1592 }
1593
1594 private class Invite extends XmppUri {
1595
1596 public String account;
1597
1598 boolean forceDialog = false;
1599
1600
1601 Invite(final String uri) {
1602 super(uri);
1603 }
1604
1605 Invite(Uri uri, boolean safeSource) {
1606 super(uri, safeSource);
1607 }
1608
1609 boolean invite() {
1610 if (!isValidJid()) {
1611 Toast.makeText(StartConversationActivity.this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
1612 return false;
1613 }
1614 if (getJid() != null) {
1615 return handleJid(this);
1616 }
1617 return false;
1618 }
1619 }
1620
1621 class TagsAdapter extends RecyclerView.Adapter<TagsAdapter.ViewHolder> {
1622 class ViewHolder extends RecyclerView.ViewHolder {
1623 protected TextView tv;
1624
1625 public ViewHolder(View v) {
1626 super(v);
1627 tv = (TextView) v;
1628 tv.setOnClickListener(view -> {
1629 String needle = mSearchEditText.getText().toString();
1630 String tag = tv.getText().toString();
1631 String[] parts = needle.split("[,\\s]+");
1632 if(needle.isEmpty()) {
1633 needle = tag;
1634 } else if (tag.toLowerCase(Locale.US).contains(parts[parts.length-1])) {
1635 needle = needle.replace(parts[parts.length-1], tag);
1636 } else {
1637 needle += ", " + tag;
1638 }
1639 mSearchEditText.setText("");
1640 mSearchEditText.append(needle);
1641 filter(needle);
1642 });
1643 }
1644
1645 public void setTag(ListItem.Tag tag) {
1646 tv.setText(tag.getName());
1647 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(StartConversationActivity.this,XEP0392Helper.rgbFromNick(tag.getName()))));
1648 }
1649 }
1650
1651 protected List<ListItem.Tag> tags = new ArrayList<>();
1652
1653 @Override
1654 public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
1655 View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_tag, null);
1656 return new ViewHolder(view);
1657 }
1658
1659 @Override
1660 public void onBindViewHolder(ViewHolder viewHolder, int i) {
1661 viewHolder.setTag(tags.get(i));
1662 }
1663
1664 @Override
1665 public int getItemCount() {
1666 return tags.size();
1667 }
1668
1669 public void setTags(final List<ListItem.Tag> tags) {
1670 ListItem.Tag channelTag = new ListItem.Tag("Channel");
1671 String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim();
1672 HashSet<String> parts = new HashSet<>(Arrays.asList(needle.split("[,\\s]+")));
1673 this.tags = tags.stream().filter(
1674 tag -> !tag.equals(channelTag) && !parts.contains(tag.getName().toLowerCase(Locale.US))
1675 ).collect(Collectors.toList());
1676 if (!parts.contains("channel") && tags.contains(channelTag)) this.tags.add(0, channelTag);
1677 notifyDataSetChanged();
1678 }
1679 }
1680}