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