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