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