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