1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.content.res.TypedArray;
6import android.databinding.DataBindingUtil;
7import android.databinding.ViewDataBinding;
8import android.graphics.drawable.Drawable;
9import android.support.annotation.AttrRes;
10import android.support.annotation.DrawableRes;
11import android.support.annotation.NonNull;
12import android.support.v4.content.ContextCompat;
13import android.support.v7.app.AlertDialog;
14import android.app.Dialog;
15import android.app.PendingIntent;
16import android.content.ActivityNotFoundException;
17import android.content.Context;
18import android.content.DialogInterface;
19import android.content.DialogInterface.OnClickListener;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.net.Uri;
23import android.os.Build;
24import android.os.Bundle;
25import android.support.v4.app.Fragment;
26import android.support.v4.app.FragmentManager;
27import android.support.v4.app.FragmentTransaction;
28import android.support.v4.app.ListFragment;
29import android.support.v4.view.MenuItemCompat;
30import android.support.v4.view.PagerAdapter;
31import android.support.v4.view.ViewPager;
32import android.support.v7.app.ActionBar;
33import android.text.Editable;
34import android.text.SpannableString;
35import android.text.Spanned;
36import android.text.TextWatcher;
37import android.text.style.TypefaceSpan;
38import android.util.Pair;
39import android.view.ContextMenu;
40import android.view.ContextMenu.ContextMenuInfo;
41import android.view.KeyEvent;
42import android.view.Menu;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.inputmethod.InputMethodManager;
47import android.widget.AdapterView;
48import android.widget.AdapterView.AdapterContextMenuInfo;
49import android.widget.AdapterView.OnItemClickListener;
50import android.widget.ArrayAdapter;
51import android.widget.AutoCompleteTextView;
52import android.widget.CheckBox;
53import android.widget.Checkable;
54import android.widget.EditText;
55import android.widget.ListView;
56import android.widget.Spinner;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import java.util.ArrayList;
61import java.util.Arrays;
62import java.util.Collections;
63import java.util.List;
64import java.util.concurrent.atomic.AtomicBoolean;
65
66import eu.siacs.conversations.Config;
67import eu.siacs.conversations.R;
68import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
69import eu.siacs.conversations.entities.Account;
70import eu.siacs.conversations.entities.Bookmark;
71import eu.siacs.conversations.entities.Contact;
72import eu.siacs.conversations.entities.Conversation;
73import eu.siacs.conversations.entities.ListItem;
74import eu.siacs.conversations.entities.Presence;
75import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
76import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
77import eu.siacs.conversations.ui.adapter.ListItemAdapter;
78import eu.siacs.conversations.ui.service.EmojiService;
79import eu.siacs.conversations.utils.XmppUri;
80import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
81import eu.siacs.conversations.xmpp.XmppConnection;
82import eu.siacs.conversations.xmpp.jid.InvalidJidException;
83import eu.siacs.conversations.xmpp.jid.Jid;
84
85public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist {
86
87 public int conference_context_id;
88 public int contact_context_id;
89 private ListPagerAdapter mListPagerAdapter;
90 private List<ListItem> contacts = new ArrayList<>();
91 private ListItemAdapter mContactsAdapter;
92 private List<ListItem> conferences = new ArrayList<>();
93 private ListItemAdapter mConferenceAdapter;
94 private List<String> mActivatedAccounts = new ArrayList<>();
95 private List<String> mKnownHosts;
96 private List<String> mKnownConferenceHosts;
97 private Invite mPendingInvite = null;
98 private EditText mSearchEditText;
99 private AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
100 private final int REQUEST_SYNC_CONTACTS = 0x28cf;
101 private final int REQUEST_CREATE_CONFERENCE = 0x39da;
102 private Dialog mCurrentDialog = null;
103
104 private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
105
106 @Override
107 public boolean onMenuItemActionExpand(MenuItem item) {
108 mSearchEditText.post(() -> {
109 mSearchEditText.requestFocus();
110 InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
111 imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
112 });
113
114 return true;
115 }
116
117 @Override
118 public boolean onMenuItemActionCollapse(MenuItem item) {
119 hideKeyboard();
120 mSearchEditText.setText("");
121 filter(null);
122 return true;
123 }
124 };
125 private boolean mHideOfflineContacts = false;
126 private ActionBar.TabListener mTabListener = new ActionBar.TabListener() {
127
128 @Override
129 public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
130 return;
131 }
132
133 @Override
134 public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
135 binding.startConversationViewPager.setCurrentItem(tab.getPosition());
136 onTabChanged();
137 }
138
139 @Override
140 public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
141 return;
142 }
143 };
144 private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
145 @Override
146 public void onPageSelected(int position) {
147 ActionBar actionBar = getSupportActionBar();
148 if (actionBar != null) {
149 actionBar.setSelectedNavigationItem(position);
150 }
151 onTabChanged();
152 }
153 };
154 private TextWatcher mSearchTextWatcher = new TextWatcher() {
155
156 @Override
157 public void afterTextChanged(Editable editable) {
158 filter(editable.toString());
159 }
160
161 @Override
162 public void beforeTextChanged(CharSequence s, int start, int count,
163 int after) {
164 }
165
166 @Override
167 public void onTextChanged(CharSequence s, int start, int before, int count) {
168 }
169 };
170
171 private TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
172 @Override
173 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
174 int pos = getSupportActionBar().getSelectedNavigationIndex();
175 if (pos == 0) {
176 if (contacts.size() == 1) {
177 openConversationForContact((Contact) contacts.get(0));
178 return true;
179 }
180 } else {
181 if (conferences.size() == 1) {
182 openConversationsForBookmark((Bookmark) conferences.get(0));
183 return true;
184 }
185 }
186 hideKeyboard();
187 mListPagerAdapter.requestFocus(pos);
188 return true;
189 }
190 };
191 private MenuItem mMenuSearchView;
192 private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
193 @Override
194 public void onTagClicked(String tag) {
195 if (mMenuSearchView != null) {
196 mMenuSearchView.expandActionView();
197 mSearchEditText.setText("");
198 mSearchEditText.append(tag);
199 filter(tag);
200 }
201 }
202 };
203 private String mInitialJid;
204 private Pair<Integer, Intent> mPostponedActivityResult;
205 private UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
206 @Override
207 public void success(final Conversation conversation) {
208 runOnUiThread(() -> {
209 hideToast();
210 switchToConversation(conversation);
211 });
212 }
213
214 @Override
215 public void error(final int errorCode, Conversation object) {
216 runOnUiThread(() -> replaceToast(getString(errorCode)));
217 }
218
219 @Override
220 public void userInputRequried(PendingIntent pi, Conversation object) {
221
222 }
223 };
224 private Toast mToast;
225 private ActivityStartConversationBinding binding;
226
227 protected void hideToast() {
228 if (mToast != null) {
229 mToast.cancel();
230 }
231 }
232
233 protected void replaceToast(String msg) {
234 hideToast();
235 mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
236 mToast.show();
237 }
238
239 @Override
240 public void onRosterUpdate() {
241 this.refreshUi();
242 }
243
244 @Override
245 public void onCreate(Bundle savedInstanceState) {
246 super.onCreate(savedInstanceState);
247 new EmojiService(this).init();
248 this.binding = DataBindingUtil.setContentView(this,R.layout.activity_start_conversation);
249 this.binding.fab.setOnClickListener((v) -> {
250 if (getSupportActionBar().getSelectedNavigationIndex() == 0) {
251 showCreateContactDialog(null, null);
252 } else {
253 showCreateConferenceDialog();
254 }
255 });
256 ActionBar actionBar = getSupportActionBar();
257 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
258
259 ActionBar.Tab mContactsTab = actionBar.newTab().setText(R.string.contacts).setTabListener(mTabListener);
260 ActionBar.Tab mConferencesTab = actionBar.newTab().setText(R.string.conferences).setTabListener(mTabListener);
261 actionBar.addTab(mContactsTab);
262 actionBar.addTab(mConferencesTab);
263
264 binding.startConversationViewPager.setOnPageChangeListener(mOnPageChangeListener);
265 mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
266 binding.startConversationViewPager.setAdapter(mListPagerAdapter);
267
268 mConferenceAdapter = new ListItemAdapter(this, conferences);
269 mContactsAdapter = new ListItemAdapter(this, contacts);
270 mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
271 this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false);
272
273 }
274
275 @Override
276 public void onStart() {
277 super.onStart();
278 final int theme = findTheme();
279 if (this.mTheme != theme) {
280 recreate();
281 } else {
282 Intent i = getIntent();
283 if (i == null || !i.hasExtra(WelcomeActivity.EXTRA_INVITE_URI)) {
284 askForContactsPermissions();
285 }
286 }
287 mConferenceAdapter.refreshSettings();
288 mContactsAdapter.refreshSettings();
289 }
290
291 @Override
292 public void onStop() {
293 if (mCurrentDialog != null) {
294 mCurrentDialog.dismiss();
295 }
296 super.onStop();
297 }
298
299 @Override
300 public void onNewIntent(Intent intent) {
301 if (xmppConnectionServiceBound) {
302 handleIntent(intent);
303 } else {
304 setIntent(intent);
305 }
306 }
307
308 protected void openConversationForContact(int position) {
309 Contact contact = (Contact) contacts.get(position);
310 openConversationForContact(contact);
311 }
312
313 protected void openConversationForContact(Contact contact) {
314 Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
315 switchToConversation(conversation);
316 }
317
318 protected void openConversationForContact() {
319 int position = contact_context_id;
320 openConversationForContact(position);
321 }
322
323 protected void openConversationForBookmark() {
324 openConversationForBookmark(conference_context_id);
325 }
326
327 protected void openConversationForBookmark(int position) {
328 Bookmark bookmark = (Bookmark) conferences.get(position);
329 openConversationsForBookmark(bookmark);
330 }
331
332 protected void shareBookmarkUri() {
333 shareBookmarkUri(conference_context_id);
334 }
335
336 protected void shareBookmarkUri(int position) {
337 Bookmark bookmark = (Bookmark) conferences.get(position);
338 Intent shareIntent = new Intent();
339 shareIntent.setAction(Intent.ACTION_SEND);
340 shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:"+bookmark.getJid().toBareJid().toString()+"?join");
341 shareIntent.setType("text/plain");
342 try {
343 startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
344 } catch (ActivityNotFoundException e) {
345 Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
346 }
347 }
348
349 protected void openConversationsForBookmark(Bookmark bookmark) {
350 Jid jid = bookmark.getJid();
351 if (jid == null) {
352 Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
353 return;
354 }
355 Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
356 bookmark.setConversation(conversation);
357 if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) {
358 bookmark.setAutojoin(true);
359 xmppConnectionService.pushBookmarks(bookmark.getAccount());
360 }
361 switchToConversation(conversation);
362 }
363
364 protected void openDetailsForContact() {
365 int position = contact_context_id;
366 Contact contact = (Contact) contacts.get(position);
367 switchToContactDetails(contact);
368 }
369
370 protected void toggleContactBlock() {
371 final int position = contact_context_id;
372 BlockContactDialog.show(this, (Contact) contacts.get(position));
373 }
374
375 protected void deleteContact() {
376 final int position = contact_context_id;
377 final Contact contact = (Contact) contacts.get(position);
378 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
379 builder.setNegativeButton(R.string.cancel, null);
380 builder.setTitle(R.string.action_delete_contact);
381 builder.setMessage(getString(R.string.remove_contact_text,
382 contact.getJid()));
383 builder.setPositiveButton(R.string.delete, new OnClickListener() {
384
385 @Override
386 public void onClick(DialogInterface dialog, int which) {
387 xmppConnectionService.deleteContactOnServer(contact);
388 filter(mSearchEditText.getText().toString());
389 }
390 });
391 builder.create().show();
392 }
393
394 protected void deleteConference() {
395 int position = conference_context_id;
396 final Bookmark bookmark = (Bookmark) conferences.get(position);
397
398 AlertDialog.Builder builder = new AlertDialog.Builder(this);
399 builder.setNegativeButton(R.string.cancel, null);
400 builder.setTitle(R.string.delete_bookmark);
401 builder.setMessage(getString(R.string.remove_bookmark_text,
402 bookmark.getJid()));
403 builder.setPositiveButton(R.string.delete, new OnClickListener() {
404
405 @Override
406 public void onClick(DialogInterface dialog, int which) {
407 bookmark.setConversation(null);
408 Account account = bookmark.getAccount();
409 account.getBookmarks().remove(bookmark);
410 xmppConnectionService.pushBookmarks(account);
411 filter(mSearchEditText.getText().toString());
412 }
413 });
414 builder.create().show();
415
416 }
417
418 @SuppressLint("InflateParams")
419 protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
420 EnterJidDialog dialog = new EnterJidDialog(
421 this, mKnownHosts, mActivatedAccounts,
422 getString(R.string.dialog_title_create_contact), getString(R.string.create),
423 prefilledJid, null, invite == null || !invite.hasFingerprints()
424 );
425
426 dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
427 if (!xmppConnectionServiceBound) {
428 return false;
429 }
430
431 final Account account = xmppConnectionService.findAccountByJid(accountJid);
432 if (account == null) {
433 return true;
434 }
435
436 final Contact contact = account.getRoster().getContact(contactJid);
437 if (invite != null && invite.getName() != null) {
438 contact.setServerName(invite.getName());
439 }
440 if (contact.isSelf()) {
441 switchToConversation(contact,null);
442 return true;
443 } else if (contact.showInRoster()) {
444 throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
445 } else {
446 xmppConnectionService.createContact(contact);
447 if (invite != null && invite.hasFingerprints()) {
448 xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
449 }
450 switchToConversation(contact, invite == null ? null : invite.getBody());
451 return true;
452 }
453 });
454
455 mCurrentDialog = dialog.show();
456 }
457
458 @SuppressLint("InflateParams")
459 protected void showJoinConferenceDialog(final String prefilledJid) {
460 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
461 builder.setTitle(R.string.dialog_title_join_conference);
462 final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null);
463 final Spinner spinner = dialogView.findViewById(R.id.account);
464 final AutoCompleteTextView jid = dialogView.findViewById(R.id.jid);
465 final TextView jabberIdDesc = dialogView.findViewById(R.id.jabber_id);
466 jabberIdDesc.setText(R.string.conference_address);
467 jid.setHint(R.string.conference_address_example);
468 jid.setAdapter(new KnownHostsAdapter(this, R.layout.simple_list_item, mKnownConferenceHosts));
469 if (prefilledJid != null) {
470 jid.append(prefilledJid);
471 }
472 populateAccountSpinner(this, mActivatedAccounts, spinner);
473 final Checkable bookmarkCheckBox = (CheckBox) dialogView
474 .findViewById(R.id.bookmark);
475 builder.setView(dialogView);
476 builder.setNegativeButton(R.string.cancel, null);
477 builder.setPositiveButton(R.string.join, null);
478 final AlertDialog dialog = builder.create();
479 dialog.show();
480 mCurrentDialog = dialog;
481 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
482 new View.OnClickListener() {
483
484 @Override
485 public void onClick(final View v) {
486 if (!xmppConnectionServiceBound) {
487 return;
488 }
489 final Account account = getSelectedAccount(spinner);
490 if (account == null) {
491 return;
492 }
493 final Jid conferenceJid;
494 try {
495 conferenceJid = Jid.fromString(jid.getText().toString());
496 } catch (final InvalidJidException e) {
497 jid.setError(getString(R.string.invalid_jid));
498 return;
499 }
500
501 if (bookmarkCheckBox.isChecked()) {
502 if (account.hasBookmarkFor(conferenceJid)) {
503 jid.setError(getString(R.string.bookmark_already_exists));
504 } else {
505 final Bookmark bookmark = new Bookmark(account, conferenceJid.toBareJid());
506 bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
507 String nick = conferenceJid.getResourcepart();
508 if (nick != null && !nick.isEmpty()) {
509 bookmark.setNick(nick);
510 }
511 account.getBookmarks().add(bookmark);
512 xmppConnectionService.pushBookmarks(account);
513 final Conversation conversation = xmppConnectionService
514 .findOrCreateConversation(account, conferenceJid, true, true, true);
515 bookmark.setConversation(conversation);
516 dialog.dismiss();
517 mCurrentDialog = null;
518 switchToConversation(conversation);
519 }
520 } else {
521 final Conversation conversation = xmppConnectionService
522 .findOrCreateConversation(account,conferenceJid, true, true, true);
523 dialog.dismiss();
524 mCurrentDialog = null;
525 switchToConversation(conversation);
526 }
527 }
528 });
529 }
530
531 private void showCreateConferenceDialog() {
532 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
533 builder.setTitle(R.string.dialog_title_create_conference);
534 final View dialogView = getLayoutInflater().inflate(R.layout.create_conference_dialog, null);
535 final Spinner spinner = dialogView.findViewById(R.id.account);
536 final EditText subject = dialogView.findViewById(R.id.subject);
537 populateAccountSpinner(this, mActivatedAccounts, spinner);
538 builder.setView(dialogView);
539 builder.setPositiveButton(R.string.choose_participants, new OnClickListener() {
540 @Override
541 public void onClick(DialogInterface dialog, int which) {
542 if (!xmppConnectionServiceBound) {
543 return;
544 }
545 final Account account = getSelectedAccount(spinner);
546 if (account == null) {
547 return;
548 }
549 Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
550 intent.putExtra("multiple", true);
551 intent.putExtra("show_enter_jid", true);
552 intent.putExtra("subject", subject.getText().toString());
553 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
554 intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
555 startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
556 }
557 });
558 builder.setNegativeButton(R.string.cancel, null);
559 mCurrentDialog = builder.create();
560 mCurrentDialog.show();
561 }
562
563 private Account getSelectedAccount(Spinner spinner) {
564 if (!spinner.isEnabled()) {
565 return null;
566 }
567 Jid jid;
568 try {
569 if (Config.DOMAIN_LOCK != null) {
570 jid = Jid.fromParts((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
571 } else {
572 jid = Jid.fromString((String) spinner.getSelectedItem());
573 }
574 } catch (final InvalidJidException e) {
575 return null;
576 }
577 return xmppConnectionService.findAccountByJid(jid);
578 }
579
580 protected void switchToConversation(Contact contact, String body) {
581 Conversation conversation = xmppConnectionService
582 .findOrCreateConversation(contact.getAccount(),
583 contact.getJid(),false,true);
584 switchToConversation(conversation, body, false);
585 }
586
587 public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
588 if (accounts.size() > 0) {
589 ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
590 adapter.setDropDownViewResource(R.layout.simple_list_item);
591 spinner.setAdapter(adapter);
592 spinner.setEnabled(true);
593 } else {
594 ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
595 R.layout.simple_list_item,
596 Arrays.asList(context.getString(R.string.no_accounts)));
597 adapter.setDropDownViewResource(R.layout.simple_list_item);
598 spinner.setAdapter(adapter);
599 spinner.setEnabled(false);
600 }
601 }
602
603 @Override
604 public boolean onCreateOptionsMenu(Menu menu) {
605 getMenuInflater().inflate(R.menu.start_conversation, menu);
606 MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
607 menuHideOffline.setChecked(this.mHideOfflineContacts);
608 mMenuSearchView = menu.findItem(R.id.action_search);
609 mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
610 View mSearchView = mMenuSearchView.getActionView();
611 mSearchEditText = mSearchView.findViewById(R.id.search_field);
612 mSearchEditText.addTextChangedListener(mSearchTextWatcher);
613 mSearchEditText.setOnEditorActionListener(mSearchDone);
614 if (mInitialJid != null) {
615 MenuItemCompat.expandActionView(mMenuSearchView);
616 mSearchEditText.append(mInitialJid);
617 filter(mInitialJid);
618 }
619 return super.onCreateOptionsMenu(menu);
620 }
621
622 @Override
623 public boolean onOptionsItemSelected(MenuItem item) {
624 switch (item.getItemId()) {
625 case R.id.action_join_conference:
626 showJoinConferenceDialog(null);
627 return true;
628 case R.id.action_scan_qr_code:
629 Intent intent = new Intent(this, UriHandlerActivity.class);
630 intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
631 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
632 startActivity(intent);
633 return true;
634 case R.id.action_hide_offline:
635 mHideOfflineContacts = !item.isChecked();
636 getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit();
637 if (mSearchEditText != null) {
638 filter(mSearchEditText.getText().toString());
639 }
640 invalidateOptionsMenu();
641 }
642 return super.onOptionsItemSelected(item);
643 }
644
645 @Override
646 public boolean onKeyUp(int keyCode, KeyEvent event) {
647 if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
648 openSearch();
649 return true;
650 }
651 int c = event.getUnicodeChar();
652 if (c > 32) {
653 if (mSearchEditText != null && !mSearchEditText.isFocused()) {
654 openSearch();
655 mSearchEditText.append(Character.toString((char) c));
656 return true;
657 }
658 }
659 return super.onKeyUp(keyCode, event);
660 }
661
662 private void openSearch() {
663 if (mMenuSearchView != null) {
664 mMenuSearchView.expandActionView();
665 }
666 }
667
668 @Override
669 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
670 if (resultCode == RESULT_OK) {
671 if (xmppConnectionServiceBound) {
672 this.mPostponedActivityResult = null;
673 if (requestCode == REQUEST_CREATE_CONFERENCE) {
674 Account account = extractAccount(intent);
675 final String subject = intent.getStringExtra("subject");
676 List<Jid> jids = new ArrayList<>();
677 if (intent.getBooleanExtra("multiple", false)) {
678 String[] toAdd = intent.getStringArrayExtra("contacts");
679 for (String item : toAdd) {
680 try {
681 jids.add(Jid.fromString(item));
682 } catch (InvalidJidException e) {
683 //ignored
684 }
685 }
686 } else {
687 try {
688 jids.add(Jid.fromString(intent.getStringExtra("contact")));
689 } catch (Exception e) {
690 //ignored
691 }
692 }
693 if (account != null && jids.size() > 0) {
694 if (xmppConnectionService.createAdhocConference(account, subject, jids, mAdhocConferenceCallback)) {
695 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
696 mToast.show();
697 }
698 }
699 }
700 } else {
701 this.mPostponedActivityResult = new Pair<>(requestCode, intent);
702 }
703 }
704 super.onActivityResult(requestCode, requestCode, intent);
705 }
706
707 private void askForContactsPermissions() {
708 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
709 if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
710 if (mRequestedContactsPermission.compareAndSet(false, true)) {
711 if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
712 AlertDialog.Builder builder = new AlertDialog.Builder(this);
713 builder.setTitle(R.string.sync_with_contacts);
714 builder.setMessage(R.string.sync_with_contacts_long);
715 builder.setPositiveButton(R.string.next, new OnClickListener() {
716 @Override
717 public void onClick(DialogInterface dialog, int which) {
718 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
719 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
720 }
721 }
722 });
723 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
724 builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
725 @Override
726 public void onDismiss(DialogInterface dialog) {
727 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
728 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
729 }
730 }
731 });
732 }
733 builder.create().show();
734 } else {
735 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0);
736 }
737 }
738 }
739 }
740 }
741
742 @Override
743 public void onRequestPermissionsResult(int requestCode,@NonNull String permissions[],@NonNull int[] grantResults) {
744 if (grantResults.length > 0)
745 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
746 if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
747 xmppConnectionService.loadPhoneContacts();
748 }
749 }
750 }
751
752 @Override
753 protected void onBackendConnected() {
754 if (mPostponedActivityResult != null) {
755 onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
756 this.mPostponedActivityResult = null;
757 }
758 this.mActivatedAccounts.clear();
759 for (Account account : xmppConnectionService.getAccounts()) {
760 if (account.getStatus() != Account.State.DISABLED) {
761 if (Config.DOMAIN_LOCK != null) {
762 this.mActivatedAccounts.add(account.getJid().getLocalpart());
763 } else {
764 this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
765 }
766 }
767 }
768 final Intent intent = getIntent();
769 final ActionBar ab = getSupportActionBar();
770 boolean init = intent != null && intent.getBooleanExtra("init", false);
771 boolean noConversations = xmppConnectionService.getConversations().size() == 0;
772 if ((init || noConversations) && ab != null) {
773 ab.setDisplayShowHomeEnabled(false);
774 ab.setDisplayHomeAsUpEnabled(false);
775 ab.setHomeButtonEnabled(false);
776 }
777 this.mKnownHosts = xmppConnectionService.getKnownHosts();
778 this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts();
779 if (this.mPendingInvite != null) {
780 mPendingInvite.invite();
781 this.mPendingInvite = null;
782 filter(null);
783 } else if (!handleIntent(getIntent())) {
784 if (mSearchEditText != null) {
785 filter(mSearchEditText.getText().toString());
786 } else {
787 filter(null);
788 }
789 } else {
790 filter(null);
791 }
792 setIntent(null);
793 }
794
795 protected boolean handleIntent(Intent intent) {
796 if (intent == null) {
797 return false;
798 }
799 final String inviteUri = intent.getStringExtra(WelcomeActivity.EXTRA_INVITE_URI);
800 if (inviteUri != null) {
801 Invite invite = new Invite(inviteUri);
802 if (invite.isJidValid()) {
803 return invite.invite();
804 }
805 }
806 if (intent.getAction() == null) {
807 return false;
808 }
809 switch (intent.getAction()) {
810 case Intent.ACTION_SENDTO:
811 case Intent.ACTION_VIEW:
812 Uri uri = intent.getData();
813 if (uri != null) {
814 Invite invite = new Invite(intent.getData(),false);
815 invite.account = intent.getStringExtra("account");
816 return invite.invite();
817 } else {
818 return false;
819 }
820 }
821 return false;
822 }
823
824 private boolean handleJid(Invite invite) {
825 List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(),invite.account);
826 if (invite.isAction(XmppUri.ACTION_JOIN)) {
827 Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
828 if (muc != null) {
829 switchToConversation(muc,invite.getBody(),false);
830 return true;
831 } else {
832 showJoinConferenceDialog(invite.getJid().toBareJid().toString());
833 return false;
834 }
835 } else if (contacts.size() == 0) {
836 showCreateContactDialog(invite.getJid().toString(), invite);
837 return false;
838 } else if (contacts.size() == 1) {
839 Contact contact = contacts.get(0);
840 if (!invite.isSafeSource() && invite.hasFingerprints()) {
841 displayVerificationWarningDialog(contact,invite);
842 } else {
843 if (invite.hasFingerprints()) {
844 if(xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
845 Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
846 }
847 }
848 if (invite.account != null) {
849 xmppConnectionService.getShortcutService().report(contact);
850 }
851 switchToConversation(contact, invite.getBody());
852 }
853 return true;
854 } else {
855 if (mMenuSearchView != null) {
856 mMenuSearchView.expandActionView();
857 mSearchEditText.setText("");
858 mSearchEditText.append(invite.getJid().toString());
859 filter(invite.getJid().toString());
860 } else {
861 mInitialJid = invite.getJid().toString();
862 }
863 return true;
864 }
865 }
866
867 private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
868 AlertDialog.Builder builder = new AlertDialog.Builder(this);
869 builder.setTitle(R.string.verify_omemo_keys);
870 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
871 final CheckBox isTrustedSource = (CheckBox) view.findViewById(R.id.trusted_source);
872 TextView warning = (TextView) view.findViewById(R.id.warning);
873 String jid = contact.getJid().toBareJid().toString();
874 SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source,jid,contact.getDisplayName()));
875 int start = spannable.toString().indexOf(jid);
876 if (start >= 0) {
877 spannable.setSpan(new TypefaceSpan("monospace"),start,start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
878 }
879 warning.setText(spannable);
880 builder.setView(view);
881 builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
882 if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
883 xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
884 }
885 switchToConversation(contact, invite.getBody());
886 });
887 builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
888 AlertDialog dialog = builder.create();
889 dialog.setCanceledOnTouchOutside(false);
890 dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
891 dialog.show();
892 }
893
894 protected void filter(String needle) {
895 if (xmppConnectionServiceBound) {
896 this.filterContacts(needle);
897 this.filterConferences(needle);
898 }
899 }
900
901 protected void filterContacts(String needle) {
902 this.contacts.clear();
903 for (Account account : xmppConnectionService.getAccounts()) {
904 if (account.getStatus() != Account.State.DISABLED) {
905 for (Contact contact : account.getRoster().getContacts()) {
906 Presence.Status s = contact.getShownStatus();
907 if (contact.showInRoster() && contact.match(this, needle)
908 && (!this.mHideOfflineContacts
909 || (needle != null && !needle.trim().isEmpty())
910 || s.compareTo(Presence.Status.OFFLINE) < 0)) {
911 this.contacts.add(contact);
912 }
913 }
914 }
915 }
916 Collections.sort(this.contacts);
917 mContactsAdapter.notifyDataSetChanged();
918 }
919
920 protected void filterConferences(String needle) {
921 this.conferences.clear();
922 for (Account account : xmppConnectionService.getAccounts()) {
923 if (account.getStatus() != Account.State.DISABLED) {
924 for (Bookmark bookmark : account.getBookmarks()) {
925 if (bookmark.match(this, needle)) {
926 this.conferences.add(bookmark);
927 }
928 }
929 }
930 }
931 Collections.sort(this.conferences);
932 mConferenceAdapter.notifyDataSetChanged();
933 }
934
935 private void onTabChanged() {
936 @DrawableRes final int fabDrawable;
937 if (getSupportActionBar().getSelectedNavigationIndex() == 0) {
938 fabDrawable = R.drawable.ic_person_add_white_24dp;
939 } else {
940 fabDrawable = R.drawable.ic_group_add_white_24dp;
941 }
942 binding.fab.setImageResource(fabDrawable);
943 invalidateOptionsMenu();
944 }
945
946 public static void launch(Context context) {
947 final Intent intent = new Intent(context,StartConversationActivity.class);
948 context.startActivity(intent);
949 }
950
951 @Override
952 public void OnUpdateBlocklist(final Status status) {
953 refreshUi();
954 }
955
956 @Override
957 protected void refreshUiReal() {
958 if (mSearchEditText != null) {
959 filter(mSearchEditText.getText().toString());
960 }
961 }
962
963 public class ListPagerAdapter extends PagerAdapter {
964 FragmentManager fragmentManager;
965 MyListFragment[] fragments;
966
967 public ListPagerAdapter(FragmentManager fm) {
968 fragmentManager = fm;
969 fragments = new MyListFragment[2];
970 }
971
972 public void requestFocus(int pos) {
973 if (fragments.length > pos) {
974 fragments[pos].getListView().requestFocus();
975 }
976 }
977
978 @Override
979 public void destroyItem(@NonNull ViewGroup container, int position,@NonNull Object object) {
980 assert (0 <= position && position < fragments.length);
981 FragmentTransaction trans = fragmentManager.beginTransaction();
982 trans.remove(fragments[position]);
983 trans.commit();
984 fragments[position] = null;
985 }
986
987 @Override
988 public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
989 Fragment fragment = getItem(position);
990 FragmentTransaction trans = fragmentManager.beginTransaction();
991 trans.add(container.getId(), fragment, "fragment:" + position);
992 trans.commit();
993 return fragment;
994 }
995
996 @Override
997 public int getCount() {
998 return fragments.length;
999 }
1000
1001 @Override
1002 public boolean isViewFromObject(@NonNull View view,@NonNull Object fragment) {
1003 return ((Fragment) fragment).getView() == view;
1004 }
1005
1006 public Fragment getItem(int position) {
1007 assert (0 <= position && position < fragments.length);
1008 if (fragments[position] == null) {
1009 final MyListFragment listFragment = new MyListFragment();
1010 if (position == 1) {
1011 listFragment.setListAdapter(mConferenceAdapter);
1012 listFragment.setContextMenu(R.menu.conference_context);
1013 listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1014 } else {
1015
1016 listFragment.setListAdapter(mContactsAdapter);
1017 listFragment.setContextMenu(R.menu.contact_context);
1018 listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p));
1019 }
1020 fragments[position] = listFragment;
1021 }
1022 return fragments[position];
1023 }
1024 }
1025
1026 public static class MyListFragment extends ListFragment {
1027 private AdapterView.OnItemClickListener mOnItemClickListener;
1028 private int mResContextMenu;
1029
1030 public void setContextMenu(final int res) {
1031 this.mResContextMenu = res;
1032 }
1033
1034 @Override
1035 public void onListItemClick(final ListView l, final View v, final int position, final long id) {
1036 if (mOnItemClickListener != null) {
1037 mOnItemClickListener.onItemClick(l, v, position, id);
1038 }
1039 }
1040
1041 public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1042 this.mOnItemClickListener = l;
1043 }
1044
1045 @Override
1046 public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1047 super.onViewCreated(view, savedInstanceState);
1048 registerForContextMenu(getListView());
1049 getListView().setFastScrollEnabled(true);
1050 getListView().setDivider(null);
1051 getListView().setDividerHeight(0);
1052 }
1053
1054 @Override
1055 public void onCreateContextMenu(final ContextMenu menu, final View v,
1056 final ContextMenuInfo menuInfo) {
1057 super.onCreateContextMenu(menu, v, menuInfo);
1058 final StartConversationActivity activity = (StartConversationActivity) getActivity();
1059 if (activity == null) {
1060 return;
1061 }
1062 activity.getMenuInflater().inflate(mResContextMenu, menu);
1063 final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1064 if (mResContextMenu == R.menu.conference_context) {
1065 activity.conference_context_id = acmi.position;
1066 } else if (mResContextMenu == R.menu.contact_context) {
1067 activity.contact_context_id = acmi.position;
1068 final Contact contact = (Contact) activity.contacts.get(acmi.position);
1069 final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1070 final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1071 if (contact.isSelf()) {
1072 showContactDetailsItem.setVisible(false);
1073 }
1074 XmppConnection xmpp = contact.getAccount().getXmppConnection();
1075 if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1076 if (contact.isBlocked()) {
1077 blockUnblockItem.setTitle(R.string.unblock_contact);
1078 } else {
1079 blockUnblockItem.setTitle(R.string.block_contact);
1080 }
1081 } else {
1082 blockUnblockItem.setVisible(false);
1083 }
1084 }
1085 }
1086
1087 @Override
1088 public boolean onContextItemSelected(final MenuItem item) {
1089 StartConversationActivity activity = (StartConversationActivity) getActivity();
1090 if (activity == null) {
1091 return true;
1092 }
1093 switch (item.getItemId()) {
1094 case R.id.context_start_conversation:
1095 activity.openConversationForContact();
1096 break;
1097 case R.id.context_contact_details:
1098 activity.openDetailsForContact();
1099 break;
1100 case R.id.context_contact_block_unblock:
1101 activity.toggleContactBlock();
1102 break;
1103 case R.id.context_delete_contact:
1104 activity.deleteContact();
1105 break;
1106 case R.id.context_join_conference:
1107 activity.openConversationForBookmark();
1108 break;
1109 case R.id.context_share_uri:
1110 activity.shareBookmarkUri();
1111 break;
1112 case R.id.context_delete_conference:
1113 activity.deleteConference();
1114 }
1115 return true;
1116 }
1117 }
1118
1119 private class Invite extends XmppUri {
1120
1121 public Invite(final Uri uri) {
1122 super(uri);
1123 }
1124
1125 public Invite(final String uri) {
1126 super(uri);
1127 }
1128
1129 public Invite(Uri uri, boolean safeSource) {
1130 super(uri,safeSource);
1131 }
1132
1133 public String account;
1134
1135 boolean invite() {
1136 if (!isJidValid()) {
1137 Toast.makeText(StartConversationActivity.this,R.string.invalid_jid,Toast.LENGTH_SHORT).show();
1138 return false;
1139 }
1140 if (getJid() != null) {
1141 return handleJid(this);
1142 }
1143 return false;
1144 }
1145 }
1146}