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