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