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