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 speedDialView.setContentDescription(getString(R.string.add_contact_or_create_or_join_group_chat));
422 }
423
424 public static boolean isValidJid(String input) {
425 try {
426 Jid jid = Jid.ofEscaped(input);
427 return !jid.isDomainJid();
428 } catch (IllegalArgumentException e) {
429 return false;
430 }
431 }
432
433 @Override
434 public void onSaveInstanceState(Bundle savedInstanceState) {
435 Intent pendingIntent = pendingViewIntent.peek();
436 savedInstanceState.putParcelable(
437 "intent", pendingIntent != null ? pendingIntent : getIntent());
438 savedInstanceState.putBoolean(
439 "requested_contacts_permission", mRequestedContactsPermission.get());
440 savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
441 savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
442 if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
443 savedInstanceState.putString(
444 "search",
445 mSearchEditText != null ? mSearchEditText.getText().toString() : null);
446 }
447 super.onSaveInstanceState(savedInstanceState);
448 }
449
450 @Override
451 public void onStart() {
452 super.onStart();
453 mConferenceAdapter.refreshSettings();
454 mContactsAdapter.refreshSettings();
455 if (pendingViewIntent.peek() == null) {
456 if (askForContactsPermissions()) {
457 return;
458 }
459 requestNotificationPermissionIfNeeded();
460 }
461 }
462
463 private void requestNotificationPermissionIfNeeded() {
464 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
465 && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
466 != PackageManager.PERMISSION_GRANTED) {
467 requestPermissions(
468 new String[] {Manifest.permission.POST_NOTIFICATIONS},
469 REQUEST_POST_NOTIFICATION);
470 }
471 }
472
473 @Override
474 public void onNewIntent(final Intent intent) {
475 super.onNewIntent(intent);
476 if (xmppConnectionServiceBound) {
477 processViewIntent(intent);
478 } else {
479 pendingViewIntent.push(intent);
480 }
481 setIntent(createLauncherIntent(this));
482 }
483
484 protected void openConversationForContact(int position) {
485 Contact contact = (Contact) contacts.get(position);
486 openConversationForContact(contact);
487 }
488
489 protected void openConversationForContact(Contact contact) {
490 Conversation conversation =
491 xmppConnectionService.findOrCreateConversation(
492 contact.getAccount(), contact.getJid(), false, true);
493 SoftKeyboardUtils.hideSoftKeyboard(this);
494 switchToConversation(conversation);
495 }
496
497 protected void openConversationForBookmark(int position) {
498 Bookmark bookmark = (Bookmark) conferences.get(position);
499 openConversationsForBookmark(bookmark);
500 }
501
502 protected void shareBookmarkUri() {
503 shareBookmarkUri(conference_context_id);
504 }
505
506 protected void shareBookmarkUri(int position) {
507 Bookmark bookmark = (Bookmark) conferences.get(position);
508 shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString());
509 }
510
511 public static void shareAsChannel(final Context context, final String address) {
512 Intent shareIntent = new Intent();
513 shareIntent.setAction(Intent.ACTION_SEND);
514 shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + address + "?join");
515 shareIntent.setType("text/plain");
516 try {
517 context.startActivity(
518 Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
519 } catch (ActivityNotFoundException e) {
520 Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT)
521 .show();
522 }
523 }
524
525 protected void openConversationsForBookmark(final Bookmark bookmark) {
526 final Jid jid = bookmark.getFullJid();
527 if (jid == null) {
528 Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
529 return;
530 }
531 final Conversation conversation =
532 xmppConnectionService.findOrCreateConversation(
533 bookmark.getAccount(), jid, true, true, true);
534 bookmark.setConversation(conversation);
535 if (!bookmark.autojoin()) {
536 bookmark.setAutojoin(true);
537 xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
538 }
539 SoftKeyboardUtils.hideSoftKeyboard(this);
540 switchToConversation(conversation);
541 }
542
543 protected void openDetailsForContact() {
544 int position = contact_context_id;
545 Contact contact = (Contact) contacts.get(position);
546 switchToContactDetails(contact);
547 }
548
549 protected void showQrForContact() {
550 int position = contact_context_id;
551 Contact contact = (Contact) contacts.get(position);
552 showQrCode("xmpp:" + contact.getJid().asBareJid().toEscapedString());
553 }
554
555 protected void toggleContactBlock() {
556 final int position = contact_context_id;
557 BlockContactDialog.show(this, (Contact) contacts.get(position));
558 }
559
560 protected void deleteContact() {
561 final int position = contact_context_id;
562 final Contact contact = (Contact) contacts.get(position);
563 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
564 builder.setNegativeButton(R.string.cancel, null);
565 builder.setTitle(R.string.action_delete_contact);
566 builder.setMessage(
567 JidDialog.style(
568 this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
569 builder.setPositiveButton(
570 R.string.delete,
571 (dialog, which) -> {
572 xmppConnectionService.deleteContactOnServer(contact);
573 filter(mSearchEditText.getText().toString());
574 });
575 builder.create().show();
576 }
577
578 protected void deleteConference() {
579 final int position = conference_context_id;
580 final Bookmark bookmark = (Bookmark) conferences.get(position);
581 final var conversation = bookmark.getConversation();
582 final boolean hasConversation = conversation != null;
583 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
584 builder.setNegativeButton(R.string.cancel, null);
585 builder.setTitle(R.string.delete_bookmark);
586 if (hasConversation) {
587 builder.setMessage(
588 JidDialog.style(
589 this,
590 R.string.remove_bookmark_and_close,
591 bookmark.getJid().toEscapedString()));
592 } else {
593 builder.setMessage(
594 JidDialog.style(
595 this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
596 }
597 builder.setPositiveButton(
598 hasConversation ? R.string.delete_and_close : R.string.delete,
599 (dialog, which) -> {
600 bookmark.setConversation(null);
601 final Account account = bookmark.getAccount();
602 xmppConnectionService.deleteBookmark(account, bookmark);
603 if (conversation != null) {
604 xmppConnectionService.archiveConversation(conversation);
605 }
606 filter(mSearchEditText.getText().toString());
607 });
608 builder.create().show();
609 }
610
611 @SuppressLint("InflateParams")
612 protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
613 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
614 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
615 if (prev != null) {
616 ft.remove(prev);
617 }
618 ft.addToBackStack(null);
619 EnterJidDialog dialog =
620 EnterJidDialog.newInstance(
621 mActivatedAccounts,
622 getString(R.string.add_contact),
623 getString(R.string.add),
624 prefilledJid,
625 invite == null ? null : invite.account,
626 invite == null || !invite.hasFingerprints(),
627 true);
628
629 dialog.setOnEnterJidDialogPositiveListener(
630 (accountJid, contactJid) -> {
631 if (!xmppConnectionServiceBound) {
632 return false;
633 }
634
635 final Account account = xmppConnectionService.findAccountByJid(accountJid);
636 if (account == null) {
637 return true;
638 }
639
640 final Contact contact = account.getRoster().getContact(contactJid);
641 if (invite != null && invite.getName() != null) {
642 contact.setServerName(invite.getName());
643 }
644 if (contact.isSelf()) {
645 switchToConversation(contact);
646 return true;
647 } else if (contact.showInRoster()) {
648 throw new EnterJidDialog.JidError(
649 getString(R.string.contact_already_exists));
650 } else {
651 final String preAuth =
652 invite == null
653 ? null
654 : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
655 xmppConnectionService.createContact(contact, true, preAuth);
656 if (invite != null && invite.hasFingerprints()) {
657 xmppConnectionService.verifyFingerprints(
658 contact, invite.getFingerprints());
659 }
660 switchToConversationDoNotAppend(
661 contact, invite == null ? null : invite.getBody());
662 return true;
663 }
664 });
665 dialog.show(ft, FRAGMENT_TAG_DIALOG);
666 }
667
668 @SuppressLint("InflateParams")
669 protected void showJoinConferenceDialog(final String prefilledJid) {
670 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
671 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
672 if (prev != null) {
673 ft.remove(prev);
674 }
675 ft.addToBackStack(null);
676 JoinConferenceDialog joinConferenceFragment =
677 JoinConferenceDialog.newInstance(prefilledJid, mActivatedAccounts);
678 joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
679 }
680
681 private void showCreatePrivateGroupChatDialog() {
682 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
683 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
684 if (prev != null) {
685 ft.remove(prev);
686 }
687 ft.addToBackStack(null);
688 CreatePrivateGroupChatDialog createConferenceFragment =
689 CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
690 createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
691 }
692
693 private void showPublicChannelDialog() {
694 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
695 Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
696 if (prev != null) {
697 ft.remove(prev);
698 }
699 ft.addToBackStack(null);
700 CreatePublicChannelDialog dialog =
701 CreatePublicChannelDialog.newInstance(mActivatedAccounts);
702 dialog.show(ft, FRAGMENT_TAG_DIALOG);
703 }
704
705 public static Account getSelectedAccount(
706 final Context context, final AutoCompleteTextView spinner) {
707 if (spinner == null || !spinner.isEnabled()) {
708 return null;
709 }
710 if (context instanceof XmppActivity) {
711 final Jid jid;
712 try {
713 jid = Jid.ofEscaped(spinner.getText().toString());
714 } catch (final IllegalArgumentException e) {
715 return null;
716 }
717 final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService;
718 if (service == null) {
719 return null;
720 }
721 return service.findAccountByJid(jid);
722 } else {
723 return null;
724 }
725 }
726
727 protected void switchToConversation(Contact contact) {
728 Conversation conversation =
729 xmppConnectionService.findOrCreateConversation(
730 contact.getAccount(), contact.getJid(), false, true);
731 switchToConversation(conversation);
732 }
733
734 protected void switchToConversationDoNotAppend(Contact contact, String body) {
735 Conversation conversation =
736 xmppConnectionService.findOrCreateConversation(
737 contact.getAccount(), contact.getJid(), false, true);
738 switchToConversationDoNotAppend(conversation, body);
739 }
740
741 @Override
742 public void invalidateOptionsMenu() {
743 boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded();
744 String text = mSearchEditText != null ? mSearchEditText.getText().toString() : "";
745 if (isExpanded) {
746 mInitialSearchValue.push(text);
747 oneShotKeyboardSuppress.set(true);
748 }
749 super.invalidateOptionsMenu();
750 }
751
752 private void updateSearchViewHint() {
753 if (binding == null || mSearchEditText == null) {
754 return;
755 }
756 if (binding.startConversationViewPager.getCurrentItem() == 0) {
757 mSearchEditText.setHint(R.string.search_contacts);
758 mSearchEditText.setContentDescription(getString(R.string.search_contacts));
759 } else {
760 mSearchEditText.setHint(R.string.search_group_chats);
761 mSearchEditText.setContentDescription(getString(R.string.search_group_chats));
762 }
763 }
764
765 @Override
766 public boolean onCreateOptionsMenu(final Menu menu) {
767 getMenuInflater().inflate(R.menu.start_conversation, menu);
768 AccountUtils.showHideMenuItems(menu);
769 final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
770 final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
771 final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
772 privacyPolicyMenuItem.setVisible(
773 BuildConfig.PRIVACY_POLICY != null
774 && QuickConversationsService.isPlayStoreFlavor());
775 qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
776 if (QuickConversationsService.isQuicksy()) {
777 menuHideOffline.setVisible(false);
778 } else {
779 menuHideOffline.setVisible(true);
780 menuHideOffline.setChecked(this.mHideOfflineContacts);
781 }
782 mMenuSearchView = menu.findItem(R.id.action_search);
783 mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
784 View mSearchView = mMenuSearchView.getActionView();
785 mSearchEditText = mSearchView.findViewById(R.id.search_field);
786 mSearchEditText.addTextChangedListener(mSearchTextWatcher);
787 mSearchEditText.setOnEditorActionListener(mSearchDone);
788 String initialSearchValue = mInitialSearchValue.pop();
789 if (initialSearchValue != null) {
790 mMenuSearchView.expandActionView();
791 mSearchEditText.append(initialSearchValue);
792 filter(initialSearchValue);
793 }
794 updateSearchViewHint();
795 return super.onCreateOptionsMenu(menu);
796 }
797
798 @Override
799 public boolean onOptionsItemSelected(MenuItem item) {
800 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
801 return false;
802 }
803 switch (item.getItemId()) {
804 case android.R.id.home:
805 navigateBack();
806 return true;
807 case R.id.action_scan_qr_code:
808 UriHandlerActivity.scan(this);
809 return true;
810 case R.id.action_hide_offline:
811 mHideOfflineContacts = !item.isChecked();
812 getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).apply();
813 if (mSearchEditText != null) {
814 filter(mSearchEditText.getText().toString());
815 }
816 invalidateOptionsMenu();
817 }
818 return super.onOptionsItemSelected(item);
819 }
820
821 @Override
822 public boolean onKeyUp(int keyCode, KeyEvent event) {
823 if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
824 openSearch();
825 return true;
826 }
827 int c = event.getUnicodeChar();
828 if (c > 32) {
829 if (mSearchEditText != null && !mSearchEditText.isFocused()) {
830 openSearch();
831 mSearchEditText.append(Character.toString((char) c));
832 return true;
833 }
834 }
835 return super.onKeyUp(keyCode, event);
836 }
837
838 private void openSearch() {
839 if (mMenuSearchView != null) {
840 mMenuSearchView.expandActionView();
841 }
842 }
843
844 @Override
845 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
846 if (resultCode == RESULT_OK) {
847 if (xmppConnectionServiceBound) {
848 this.mPostponedActivityResult = null;
849 if (requestCode == REQUEST_CREATE_CONFERENCE) {
850 Account account = extractAccount(intent);
851 final String name =
852 intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
853 final List<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
854 if (account != null && jids.size() > 0) {
855 if (xmppConnectionService.createAdhocConference(
856 account, name, jids, mAdhocConferenceCallback)) {
857 mToast =
858 Toast.makeText(
859 this, R.string.creating_conference, Toast.LENGTH_LONG);
860 mToast.show();
861 }
862 }
863 }
864 } else {
865 this.mPostponedActivityResult = new Pair<>(requestCode, intent);
866 }
867 }
868 super.onActivityResult(requestCode, requestCode, intent);
869 }
870
871 private boolean askForContactsPermissions() {
872 if (!QuickConversationsService.isContactListIntegration(this)) {
873 return false;
874 }
875 if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
876 == PackageManager.PERMISSION_GRANTED) {
877 return false;
878 }
879 if (mRequestedContactsPermission.compareAndSet(false, true)) {
880 final ImmutableList.Builder<String> permissionBuilder = new ImmutableList.Builder<>();
881 permissionBuilder.add(Manifest.permission.READ_CONTACTS);
882 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
883 permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS);
884 }
885 final String[] permission = permissionBuilder.build().toArray(new String[0]);
886 final String consent =
887 PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
888 .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
889 final boolean requiresConsent =
890 (QuickConversationsService.isQuicksy()
891 || QuickConversationsService.isPlayStoreFlavor())
892 && !"agreed".equals(consent);
893 if (requiresConsent && "declined".equals(consent)) {
894 Log.d(
895 Config.LOGTAG,
896 "not asking for contacts permission because consent has been declined");
897 return false;
898 }
899 if (requiresConsent
900 || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
901 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
902 final AtomicBoolean requestPermission = new AtomicBoolean(false);
903 if (QuickConversationsService.isQuicksy()) {
904 builder.setTitle(R.string.quicksy_wants_your_consent);
905 builder.setMessage(
906 Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
907 } else {
908 builder.setTitle(R.string.sync_with_contacts);
909 builder.setMessage(
910 getString(
911 R.string.sync_with_contacts_long,
912 getString(R.string.app_name)));
913 }
914 @StringRes int confirmButtonText;
915 if (requiresConsent) {
916 confirmButtonText = R.string.agree_and_continue;
917 } else {
918 confirmButtonText = R.string.next;
919 }
920 builder.setPositiveButton(
921 confirmButtonText,
922 (dialog, which) -> {
923 if (requiresConsent) {
924 PreferenceManager.getDefaultSharedPreferences(
925 getApplicationContext())
926 .edit()
927 .putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
928 .apply();
929 }
930 if (requestPermission.compareAndSet(false, true)) {
931 requestPermissions(permission, REQUEST_SYNC_CONTACTS);
932 }
933 });
934 if (requiresConsent) {
935 builder.setNegativeButton(
936 R.string.decline,
937 (dialog, which) ->
938 PreferenceManager.getDefaultSharedPreferences(
939 getApplicationContext())
940 .edit()
941 .putString(
942 PREF_KEY_CONTACT_INTEGRATION_CONSENT,
943 "declined")
944 .apply());
945 } else {
946 builder.setOnDismissListener(
947 dialog -> {
948 if (requestPermission.compareAndSet(false, true)) {
949 requestPermissions(permission, REQUEST_SYNC_CONTACTS);
950 }
951 });
952 }
953 builder.setCancelable(requiresConsent);
954 final AlertDialog dialog = builder.create();
955 dialog.setCanceledOnTouchOutside(requiresConsent);
956 dialog.setOnShowListener(
957 dialogInterface -> {
958 final TextView tv = dialog.findViewById(android.R.id.message);
959 if (tv != null) {
960 tv.setMovementMethod(LinkMovementMethod.getInstance());
961 }
962 });
963 dialog.show();
964 } else {
965 requestPermissions(permission, REQUEST_SYNC_CONTACTS);
966 }
967 }
968 return true;
969 }
970
971 @Override
972 public void onRequestPermissionsResult(
973 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
974 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
975 if (grantResults.length > 0)
976 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
977 UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
978 if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
979 if (QuickConversationsService.isQuicksy()) {
980 setRefreshing(true);
981 }
982 xmppConnectionService.loadPhoneContacts();
983 xmppConnectionService.startContactObserver();
984 }
985 }
986 }
987
988 private void configureHomeButton() {
989 final ActionBar actionBar = getSupportActionBar();
990 if (actionBar == null) {
991 return;
992 }
993 boolean openConversations =
994 !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
995 actionBar.setDisplayHomeAsUpEnabled(openConversations);
996 actionBar.setDisplayHomeAsUpEnabled(openConversations);
997 }
998
999 @Override
1000 protected void onBackendConnected() {
1001 if (QuickConversationsService.isContactListIntegration(this)
1002 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
1003 || checkSelfPermission(Manifest.permission.READ_CONTACTS)
1004 == PackageManager.PERMISSION_GRANTED)) {
1005 xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
1006 }
1007 if (mPostponedActivityResult != null) {
1008 onActivityResult(
1009 mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
1010 this.mPostponedActivityResult = null;
1011 }
1012 this.mActivatedAccounts.clear();
1013 this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService));
1014 configureHomeButton();
1015 Intent intent = pendingViewIntent.pop();
1016 if (intent != null && processViewIntent(intent)) {
1017 filter(null);
1018 } else {
1019 if (mSearchEditText != null) {
1020 filter(mSearchEditText.getText().toString());
1021 } else {
1022 filter(null);
1023 }
1024 }
1025 Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
1026 if (fragment instanceof OnBackendConnected) {
1027 Log.d(Config.LOGTAG, "calling on backend connected on dialog");
1028 ((OnBackendConnected) fragment).onBackendConnected();
1029 }
1030 if (QuickConversationsService.isQuicksy()) {
1031 setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1032 }
1033 if (QuickConversationsService.isConversations()
1034 && AccountUtils.hasEnabledAccounts(xmppConnectionService)
1035 && this.contacts.size() == 0
1036 && this.conferences.size() == 0
1037 && mOpenedFab.compareAndSet(false, true)) {
1038 binding.speedDial.open();
1039 }
1040 }
1041
1042 protected boolean processViewIntent(@NonNull Intent intent) {
1043 final String inviteUri = intent.getStringExtra(EXTRA_INVITE_URI);
1044 if (inviteUri != null) {
1045 final Invite invite = new Invite(inviteUri);
1046 invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1047 if (invite.isValidJid()) {
1048 return invite.invite();
1049 }
1050 }
1051 final String action = intent.getAction();
1052 if (action == null) {
1053 return false;
1054 }
1055 switch (action) {
1056 case Intent.ACTION_SENDTO:
1057 case Intent.ACTION_VIEW:
1058 Uri uri = intent.getData();
1059 if (uri != null) {
1060 Invite invite =
1061 new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
1062 invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1063 invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
1064 return invite.invite();
1065 } else {
1066 return false;
1067 }
1068 }
1069 return false;
1070 }
1071
1072 private boolean handleJid(Invite invite) {
1073 List<Contact> contacts =
1074 xmppConnectionService.findContacts(invite.getJid(), invite.account);
1075 if (invite.isAction(XmppUri.ACTION_JOIN)) {
1076 Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
1077 if (muc != null && !invite.forceDialog) {
1078 switchToConversationDoNotAppend(muc, invite.getBody());
1079 return true;
1080 } else {
1081 showJoinConferenceDialog(invite.getJid().asBareJid().toEscapedString());
1082 return false;
1083 }
1084 } else if (contacts.size() == 0) {
1085 showCreateContactDialog(invite.getJid().toEscapedString(), invite);
1086 return false;
1087 } else if (contacts.size() == 1) {
1088 Contact contact = contacts.get(0);
1089 if (!invite.isSafeSource() && invite.hasFingerprints()) {
1090 displayVerificationWarningDialog(contact, invite);
1091 } else {
1092 if (invite.hasFingerprints()) {
1093 if (xmppConnectionService.verifyFingerprints(
1094 contact, invite.getFingerprints())) {
1095 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT)
1096 .show();
1097 }
1098 }
1099 if (invite.account != null) {
1100 xmppConnectionService.getShortcutService().report(contact);
1101 }
1102 switchToConversationDoNotAppend(contact, invite.getBody());
1103 }
1104 return true;
1105 } else {
1106 if (mMenuSearchView != null) {
1107 mMenuSearchView.expandActionView();
1108 mSearchEditText.setText("");
1109 mSearchEditText.append(invite.getJid().toEscapedString());
1110 filter(invite.getJid().toEscapedString());
1111 } else {
1112 mInitialSearchValue.push(invite.getJid().toEscapedString());
1113 }
1114 return true;
1115 }
1116 }
1117
1118 private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
1119 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1120 builder.setTitle(R.string.verify_omemo_keys);
1121 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
1122 final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
1123 TextView warning = view.findViewById(R.id.warning);
1124 warning.setText(
1125 JidDialog.style(
1126 this,
1127 R.string.verifying_omemo_keys_trusted_source,
1128 contact.getJid().asBareJid().toEscapedString(),
1129 contact.getDisplayName()));
1130 builder.setView(view);
1131 builder.setPositiveButton(
1132 R.string.confirm,
1133 (dialog, which) -> {
1134 if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
1135 xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
1136 }
1137 switchToConversationDoNotAppend(contact, invite.getBody());
1138 });
1139 builder.setNegativeButton(
1140 R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
1141 AlertDialog dialog = builder.create();
1142 dialog.setCanceledOnTouchOutside(false);
1143 dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
1144 dialog.show();
1145 }
1146
1147 protected void filter(String needle) {
1148 if (xmppConnectionServiceBound) {
1149 this.filterContacts(needle);
1150 this.filterConferences(needle);
1151 }
1152 }
1153
1154 protected void filterContacts(String needle) {
1155 this.contacts.clear();
1156 final List<Account> accounts = xmppConnectionService.getAccounts();
1157 for (final Account account : accounts) {
1158 if (account.isEnabled()) {
1159 for (Contact contact : account.getRoster().getContacts()) {
1160 Presence.Status s = contact.getShownStatus();
1161 if (contact.showInContactList()
1162 && contact.match(this, needle)
1163 && (!this.mHideOfflineContacts
1164 || (needle != null && !needle.trim().isEmpty())
1165 || s.compareTo(Presence.Status.OFFLINE) < 0)) {
1166 this.contacts.add(contact);
1167 }
1168 }
1169 }
1170 }
1171 Collections.sort(this.contacts);
1172 mContactsAdapter.notifyDataSetChanged();
1173 }
1174
1175 protected void filterConferences(String needle) {
1176 this.conferences.clear();
1177 for (final Account account : xmppConnectionService.getAccounts()) {
1178 if (account.isEnabled()) {
1179 for (final Bookmark bookmark : account.getBookmarks()) {
1180 if (bookmark.match(this, needle)) {
1181 this.conferences.add(bookmark);
1182 }
1183 }
1184 }
1185 }
1186 Collections.sort(this.conferences);
1187 mConferenceAdapter.notifyDataSetChanged();
1188 }
1189
1190 @Override
1191 public void OnUpdateBlocklist(final Status status) {
1192 refreshUi();
1193 }
1194
1195 @Override
1196 protected void refreshUiReal() {
1197 if (mSearchEditText != null) {
1198 filter(mSearchEditText.getText().toString());
1199 }
1200 configureHomeButton();
1201 if (QuickConversationsService.isQuicksy()) {
1202 setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1203 }
1204 }
1205
1206 @Override
1207 public void onBackPressed() {
1208 if (binding.speedDial.isOpen()) {
1209 binding.speedDial.close();
1210 return;
1211 }
1212 navigateBack();
1213 }
1214
1215 private void navigateBack() {
1216 if (!createdByViewIntent
1217 && xmppConnectionService != null
1218 && !xmppConnectionService.isConversationsListEmpty(null)) {
1219 Intent intent = new Intent(this, ConversationsActivity.class);
1220 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
1221 startActivity(intent);
1222 }
1223 finish();
1224 }
1225
1226 @Override
1227 public void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String name) {
1228 if (!xmppConnectionServiceBound) {
1229 return;
1230 }
1231 final Account account = getSelectedAccount(this, spinner);
1232 if (account == null) {
1233 return;
1234 }
1235 Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
1236 intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false);
1237 intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true);
1238 intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim());
1239 intent.putExtra(
1240 ChooseContactActivity.EXTRA_ACCOUNT,
1241 account.getJid().asBareJid().toEscapedString());
1242 intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
1243 startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
1244 }
1245
1246 @Override
1247 public void onJoinDialogPositiveClick(
1248 final Dialog dialog,
1249 final AutoCompleteTextView spinner,
1250 final TextInputLayout layout,
1251 final AutoCompleteTextView jid) {
1252 if (!xmppConnectionServiceBound) {
1253 return;
1254 }
1255 final Account account = getSelectedAccount(this, spinner);
1256 if (account == null) {
1257 return;
1258 }
1259 final String input = jid.getText().toString().trim();
1260 Jid conferenceJid;
1261 try {
1262 conferenceJid = Jid.ofEscaped(input);
1263 } catch (final IllegalArgumentException e) {
1264 final XmppUri xmppUri = new XmppUri(input);
1265 if (xmppUri.isValidJid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) {
1266 final Editable editable = jid.getEditableText();
1267 editable.clear();
1268 editable.append(xmppUri.getJid().toEscapedString());
1269 conferenceJid = xmppUri.getJid();
1270 } else {
1271 layout.setError(getString(R.string.invalid_jid));
1272 return;
1273 }
1274 }
1275 final var existingBookmark = account.getBookmark(conferenceJid);
1276 if (existingBookmark != null) {
1277 openConversationsForBookmark(existingBookmark);
1278 } else {
1279 final var bookmark = new Bookmark(account, conferenceJid.asBareJid());
1280 bookmark.setAutojoin(true);
1281 final String nick = conferenceJid.getResource();
1282 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
1283 bookmark.setNick(nick);
1284 }
1285 xmppConnectionService.createBookmark(account, bookmark);
1286 final Conversation conversation =
1287 xmppConnectionService.findOrCreateConversation(
1288 account, conferenceJid, true, true, true);
1289 bookmark.setConversation(conversation);
1290 switchToConversation(conversation);
1291 }
1292 dialog.dismiss();
1293 }
1294
1295 @Override
1296 public void onConversationUpdate() {
1297 refreshUi();
1298 }
1299
1300 @Override
1301 public void onRefresh() {
1302 Log.d(Config.LOGTAG, "user requested to refresh");
1303 if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) {
1304 xmppConnectionService.getQuickConversationsService().considerSyncBackground(true);
1305 }
1306 }
1307
1308 private void setRefreshing(boolean refreshing) {
1309 MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0);
1310 if (fragment != null) {
1311 fragment.setRefreshing(refreshing);
1312 }
1313 }
1314
1315 @Override
1316 public void onCreatePublicChannel(Account account, String name, Jid address) {
1317 mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG);
1318 mToast.show();
1319 xmppConnectionService.createPublicChannel(
1320 account,
1321 name,
1322 address,
1323 new UiCallback<Conversation>() {
1324 @Override
1325 public void success(Conversation conversation) {
1326 runOnUiThread(
1327 () -> {
1328 hideToast();
1329 switchToConversation(conversation);
1330 });
1331 }
1332
1333 @Override
1334 public void error(int errorCode, Conversation conversation) {
1335 runOnUiThread(
1336 () -> {
1337 replaceToast(getString(errorCode));
1338 switchToConversation(conversation);
1339 });
1340 }
1341
1342 @Override
1343 public void userInputRequired(PendingIntent pi, Conversation object) {}
1344 });
1345 }
1346
1347 public static class MyListFragment extends SwipeRefreshListFragment {
1348 private AdapterView.OnItemClickListener mOnItemClickListener;
1349 private int mResContextMenu;
1350
1351 public void setContextMenu(final int res) {
1352 this.mResContextMenu = res;
1353 }
1354
1355 @Override
1356 public void onListItemClick(
1357 final ListView l, final View v, final int position, final long id) {
1358 if (mOnItemClickListener != null) {
1359 mOnItemClickListener.onItemClick(l, v, position, id);
1360 }
1361 }
1362
1363 public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1364 this.mOnItemClickListener = l;
1365 }
1366
1367 @Override
1368 public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1369 super.onViewCreated(view, savedInstanceState);
1370 registerForContextMenu(getListView());
1371 getListView().setFastScrollEnabled(true);
1372 getListView().setDivider(null);
1373 getListView().setDividerHeight(0);
1374 }
1375
1376 @Override
1377 public void onCreateContextMenu(
1378 @NonNull final ContextMenu menu,
1379 @NonNull final View v,
1380 final ContextMenuInfo menuInfo) {
1381 super.onCreateContextMenu(menu, v, menuInfo);
1382 final StartConversationActivity activity = (StartConversationActivity) getActivity();
1383 if (activity == null) {
1384 return;
1385 }
1386 activity.getMenuInflater().inflate(mResContextMenu, menu);
1387 final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1388 if (mResContextMenu == R.menu.conference_context) {
1389 activity.conference_context_id = acmi.position;
1390 final Bookmark bookmark = (Bookmark) activity.conferences.get(acmi.position);
1391 final Conversation conversation = bookmark.getConversation();
1392 final MenuItem share = menu.findItem(R.id.context_share_uri);
1393 final MenuItem delete = menu.findItem(R.id.context_delete_conference);
1394 if (conversation != null) {
1395 delete.setTitle(R.string.delete_and_close);
1396 } else {
1397 delete.setTitle(R.string.delete_bookmark);
1398 }
1399 share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous());
1400 } else if (mResContextMenu == R.menu.contact_context) {
1401 activity.contact_context_id = acmi.position;
1402 final Contact contact = (Contact) activity.contacts.get(acmi.position);
1403 final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1404 final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1405 final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact);
1406 if (contact.isSelf()) {
1407 showContactDetailsItem.setVisible(false);
1408 }
1409 deleteContactMenuItem.setVisible(
1410 contact.showInRoster()
1411 && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER));
1412 final XmppConnection xmpp = contact.getAccount().getXmppConnection();
1413 if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1414 if (contact.isBlocked()) {
1415 blockUnblockItem.setTitle(R.string.unblock_contact);
1416 } else {
1417 blockUnblockItem.setTitle(R.string.block_contact);
1418 }
1419 } else {
1420 blockUnblockItem.setVisible(false);
1421 }
1422 }
1423 }
1424
1425 @Override
1426 public boolean onContextItemSelected(final MenuItem item) {
1427 StartConversationActivity activity = (StartConversationActivity) getActivity();
1428 if (activity == null) {
1429 return true;
1430 }
1431 switch (item.getItemId()) {
1432 case R.id.context_contact_details:
1433 activity.openDetailsForContact();
1434 break;
1435 case R.id.context_show_qr:
1436 activity.showQrForContact();
1437 break;
1438 case R.id.context_contact_block_unblock:
1439 activity.toggleContactBlock();
1440 break;
1441 case R.id.context_delete_contact:
1442 activity.deleteContact();
1443 break;
1444 case R.id.context_share_uri:
1445 activity.shareBookmarkUri();
1446 break;
1447 case R.id.context_delete_conference:
1448 activity.deleteConference();
1449 }
1450 return true;
1451 }
1452 }
1453
1454 public class ListPagerAdapter extends PagerAdapter {
1455 private final FragmentManager fragmentManager;
1456 private final MyListFragment[] fragments;
1457
1458 ListPagerAdapter(FragmentManager fm) {
1459 fragmentManager = fm;
1460 fragments = new MyListFragment[2];
1461 }
1462
1463 public void requestFocus(int pos) {
1464 if (fragments.length > pos) {
1465 fragments[pos].getListView().requestFocus();
1466 }
1467 }
1468
1469 @Override
1470 public void destroyItem(
1471 @NonNull ViewGroup container, int position, @NonNull Object object) {
1472 FragmentTransaction trans = fragmentManager.beginTransaction();
1473 trans.remove(fragments[position]);
1474 trans.commit();
1475 fragments[position] = null;
1476 }
1477
1478 @NonNull
1479 @Override
1480 public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
1481 final Fragment fragment = getItem(position);
1482 final FragmentTransaction trans = fragmentManager.beginTransaction();
1483 trans.add(container.getId(), fragment, "fragment:" + position);
1484 try {
1485 trans.commit();
1486 } catch (IllegalStateException e) {
1487 // ignore
1488 }
1489 return fragment;
1490 }
1491
1492 @Override
1493 public int getCount() {
1494 return fragments.length;
1495 }
1496
1497 @Override
1498 public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) {
1499 return ((Fragment) fragment).getView() == view;
1500 }
1501
1502 @Nullable
1503 @Override
1504 public CharSequence getPageTitle(int position) {
1505 switch (position) {
1506 case 0:
1507 return getResources().getString(R.string.contacts);
1508 case 1:
1509 return getResources().getString(R.string.group_chats);
1510 default:
1511 return super.getPageTitle(position);
1512 }
1513 }
1514
1515 Fragment getItem(int position) {
1516 if (fragments[position] == null) {
1517 final MyListFragment listFragment = new MyListFragment();
1518 if (position == 1) {
1519 listFragment.setListAdapter(mConferenceAdapter);
1520 listFragment.setContextMenu(R.menu.conference_context);
1521 listFragment.setOnListItemClickListener(
1522 (arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1523 } else {
1524 listFragment.setListAdapter(mContactsAdapter);
1525 listFragment.setContextMenu(R.menu.contact_context);
1526 listFragment.setOnListItemClickListener(
1527 (arg0, arg1, p, arg3) -> openConversationForContact(p));
1528 if (QuickConversationsService.isQuicksy()) {
1529 listFragment.setOnRefreshListener(StartConversationActivity.this);
1530 }
1531 }
1532 fragments[position] = listFragment;
1533 }
1534 return fragments[position];
1535 }
1536 }
1537
1538 public static void addInviteUri(Intent to, Intent from) {
1539 if (from != null && from.hasExtra(EXTRA_INVITE_URI)) {
1540 final String invite = from.getStringExtra(EXTRA_INVITE_URI);
1541 to.putExtra(EXTRA_INVITE_URI, invite);
1542 }
1543 }
1544
1545 private class Invite extends XmppUri {
1546
1547 public String account;
1548
1549 boolean forceDialog = false;
1550
1551 Invite(final String uri) {
1552 super(uri);
1553 }
1554
1555 Invite(Uri uri, boolean safeSource) {
1556 super(uri, safeSource);
1557 }
1558
1559 boolean invite() {
1560 if (!isValidJid()) {
1561 Toast.makeText(
1562 StartConversationActivity.this,
1563 R.string.invalid_jid,
1564 Toast.LENGTH_SHORT)
1565 .show();
1566 return false;
1567 }
1568 if (getJid() != null) {
1569 return handleJid(this);
1570 }
1571 return false;
1572 }
1573 }
1574}