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