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 askForContactsPermissions();
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 conversation.setBookmark(bookmark);
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.unregisterConversation();
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 (contact.showInRoster()) {
424 throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
425 } else {
426 xmppConnectionService.createContact(contact);
427 if (invite != null && invite.hasFingerprints()) {
428 xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
429 }
430 switchToConversation(contact, invite == null ? null : invite.getBody());
431 return true;
432 }
433 });
434
435 mCurrentDialog = dialog.show();
436 }
437
438 @SuppressLint("InflateParams")
439 protected void showJoinConferenceDialog(final String prefilledJid) {
440 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
441 builder.setTitle(R.string.dialog_title_join_conference);
442 final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null);
443 final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
444 final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
445 final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
446 jabberIdDesc.setText(R.string.conference_address);
447 jid.setHint(R.string.conference_address_example);
448 jid.setAdapter(new KnownHostsAdapter(this, R.layout.simple_list_item, mKnownConferenceHosts));
449 if (prefilledJid != null) {
450 jid.append(prefilledJid);
451 }
452 populateAccountSpinner(this, mActivatedAccounts, spinner);
453 final Checkable bookmarkCheckBox = (CheckBox) dialogView
454 .findViewById(R.id.bookmark);
455 builder.setView(dialogView);
456 builder.setNegativeButton(R.string.cancel, null);
457 builder.setPositiveButton(R.string.join, null);
458 final AlertDialog dialog = builder.create();
459 dialog.show();
460 mCurrentDialog = dialog;
461 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
462 new View.OnClickListener() {
463
464 @Override
465 public void onClick(final View v) {
466 if (!xmppConnectionServiceBound) {
467 return;
468 }
469 final Account account = getSelectedAccount(spinner);
470 if (account == null) {
471 return;
472 }
473 final Jid conferenceJid;
474 try {
475 conferenceJid = Jid.fromString(jid.getText().toString());
476 } catch (final InvalidJidException e) {
477 jid.setError(getString(R.string.invalid_jid));
478 return;
479 }
480
481 if (bookmarkCheckBox.isChecked()) {
482 if (account.hasBookmarkFor(conferenceJid)) {
483 jid.setError(getString(R.string.bookmark_already_exists));
484 } else {
485 final Bookmark bookmark = new Bookmark(account, conferenceJid.toBareJid());
486 bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
487 String nick = conferenceJid.getResourcepart();
488 if (nick != null && !nick.isEmpty()) {
489 bookmark.setNick(nick);
490 }
491 account.getBookmarks().add(bookmark);
492 xmppConnectionService.pushBookmarks(account);
493 final Conversation conversation = xmppConnectionService
494 .findOrCreateConversation(account, conferenceJid, true, true, true);
495 conversation.setBookmark(bookmark);
496 dialog.dismiss();
497 mCurrentDialog = null;
498 switchToConversation(conversation);
499 }
500 } else {
501 final Conversation conversation = xmppConnectionService
502 .findOrCreateConversation(account,conferenceJid, true, true, true);
503 dialog.dismiss();
504 mCurrentDialog = null;
505 switchToConversation(conversation);
506 }
507 }
508 });
509 }
510
511 private void showCreateConferenceDialog() {
512 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
513 builder.setTitle(R.string.dialog_title_create_conference);
514 final View dialogView = getLayoutInflater().inflate(R.layout.create_conference_dialog, null);
515 final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
516 final EditText subject = (EditText) dialogView.findViewById(R.id.subject);
517 populateAccountSpinner(this, mActivatedAccounts, spinner);
518 builder.setView(dialogView);
519 builder.setPositiveButton(R.string.choose_participants, new OnClickListener() {
520 @Override
521 public void onClick(DialogInterface dialog, int which) {
522 if (!xmppConnectionServiceBound) {
523 return;
524 }
525 final Account account = getSelectedAccount(spinner);
526 if (account == null) {
527 return;
528 }
529 Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
530 intent.putExtra("multiple", true);
531 intent.putExtra("show_enter_jid", true);
532 intent.putExtra("subject", subject.getText().toString());
533 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
534 intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
535 startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
536 }
537 });
538 builder.setNegativeButton(R.string.cancel, null);
539 mCurrentDialog = builder.create();
540 mCurrentDialog.show();
541 }
542
543 private Account getSelectedAccount(Spinner spinner) {
544 if (!spinner.isEnabled()) {
545 return null;
546 }
547 Jid jid;
548 try {
549 if (Config.DOMAIN_LOCK != null) {
550 jid = Jid.fromParts((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
551 } else {
552 jid = Jid.fromString((String) spinner.getSelectedItem());
553 }
554 } catch (final InvalidJidException e) {
555 return null;
556 }
557 return xmppConnectionService.findAccountByJid(jid);
558 }
559
560 protected void switchToConversation(Contact contact, String body) {
561 Conversation conversation = xmppConnectionService
562 .findOrCreateConversation(contact.getAccount(),
563 contact.getJid(),false,true);
564 switchToConversation(conversation, body, false);
565 }
566
567 public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
568 if (accounts.size() > 0) {
569 ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
570 adapter.setDropDownViewResource(R.layout.simple_list_item);
571 spinner.setAdapter(adapter);
572 spinner.setEnabled(true);
573 } else {
574 ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
575 R.layout.simple_list_item,
576 Arrays.asList(context.getString(R.string.no_accounts)));
577 adapter.setDropDownViewResource(R.layout.simple_list_item);
578 spinner.setAdapter(adapter);
579 spinner.setEnabled(false);
580 }
581 }
582
583 @Override
584 public boolean onCreateOptionsMenu(Menu menu) {
585 getMenuInflater().inflate(R.menu.start_conversation, menu);
586 MenuItem menuCreateContact = menu.findItem(R.id.action_create_contact);
587 MenuItem menuCreateConference = menu.findItem(R.id.action_conference);
588 MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
589 menuHideOffline.setChecked(this.mHideOfflineContacts);
590 mMenuSearchView = menu.findItem(R.id.action_search);
591 mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
592 View mSearchView = mMenuSearchView.getActionView();
593 mSearchEditText = (EditText) mSearchView
594 .findViewById(R.id.search_field);
595 mSearchEditText.addTextChangedListener(mSearchTextWatcher);
596 mSearchEditText.setOnEditorActionListener(mSearchDone);
597 if (getActionBar().getSelectedNavigationIndex() == 0) {
598 menuCreateConference.setVisible(false);
599 } else {
600 menuCreateContact.setVisible(false);
601 }
602 if (mInitialJid != null) {
603 mMenuSearchView.expandActionView();
604 mSearchEditText.append(mInitialJid);
605 filter(mInitialJid);
606 }
607 return super.onCreateOptionsMenu(menu);
608 }
609
610 @Override
611 public boolean onOptionsItemSelected(MenuItem item) {
612 switch (item.getItemId()) {
613 case R.id.action_create_contact:
614 showCreateContactDialog(null, null);
615 return true;
616 case R.id.action_join_conference:
617 showJoinConferenceDialog(null);
618 return true;
619 case R.id.action_create_conference:
620 showCreateConferenceDialog();
621 return true;
622 case R.id.action_scan_qr_code:
623 Intent intent = new Intent(this, UriHandlerActivity.class);
624 intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
625 startActivity(intent);
626 return true;
627 case R.id.action_hide_offline:
628 mHideOfflineContacts = !item.isChecked();
629 getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit();
630 if (mSearchEditText != null) {
631 filter(mSearchEditText.getText().toString());
632 }
633 invalidateOptionsMenu();
634 }
635 return super.onOptionsItemSelected(item);
636 }
637
638 @Override
639 public boolean onKeyUp(int keyCode, KeyEvent event) {
640 if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
641 openSearch();
642 return true;
643 }
644 int c = event.getUnicodeChar();
645 if (c > 32) {
646 if (mSearchEditText != null && !mSearchEditText.isFocused()) {
647 openSearch();
648 mSearchEditText.append(Character.toString((char) c));
649 return true;
650 }
651 }
652 return super.onKeyUp(keyCode, event);
653 }
654
655 private void openSearch() {
656 if (mMenuSearchView != null) {
657 mMenuSearchView.expandActionView();
658 }
659 }
660
661 @Override
662 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
663 if (resultCode == RESULT_OK) {
664 if (xmppConnectionServiceBound) {
665 this.mPostponedActivityResult = null;
666 if (requestCode == REQUEST_CREATE_CONFERENCE) {
667 Account account = extractAccount(intent);
668 final String subject = intent.getStringExtra("subject");
669 List<Jid> jids = new ArrayList<>();
670 if (intent.getBooleanExtra("multiple", false)) {
671 String[] toAdd = intent.getStringArrayExtra("contacts");
672 for (String item : toAdd) {
673 try {
674 jids.add(Jid.fromString(item));
675 } catch (InvalidJidException e) {
676 //ignored
677 }
678 }
679 } else {
680 try {
681 jids.add(Jid.fromString(intent.getStringExtra("contact")));
682 } catch (Exception e) {
683 //ignored
684 }
685 }
686 if (account != null && jids.size() > 0) {
687 if (xmppConnectionService.createAdhocConference(account, subject, jids, mAdhocConferenceCallback)) {
688 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
689 mToast.show();
690 }
691 }
692 }
693 } else {
694 this.mPostponedActivityResult = new Pair<>(requestCode, intent);
695 }
696 }
697 super.onActivityResult(requestCode, requestCode, intent);
698 }
699
700 private void askForContactsPermissions() {
701 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
702 if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
703 if (mRequestedContactsPermission.compareAndSet(false, true)) {
704 if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
705 AlertDialog.Builder builder = new AlertDialog.Builder(this);
706 builder.setTitle(R.string.sync_with_contacts);
707 builder.setMessage(R.string.sync_with_contacts_long);
708 builder.setPositiveButton(R.string.next, new OnClickListener() {
709 @Override
710 public void onClick(DialogInterface dialog, int which) {
711 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
712 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
713 }
714 }
715 });
716 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
717 builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
718 @Override
719 public void onDismiss(DialogInterface dialog) {
720 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
721 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
722 }
723 }
724 });
725 }
726 builder.create().show();
727 } else {
728 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0);
729 }
730 }
731 }
732 }
733 }
734
735 @Override
736 public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
737 if (grantResults.length > 0)
738 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
739 if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
740 xmppConnectionService.loadPhoneContacts();
741 }
742 }
743 }
744
745 @Override
746 protected void onBackendConnected() {
747 if (mPostponedActivityResult != null) {
748 onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
749 this.mPostponedActivityResult = null;
750 }
751 this.mActivatedAccounts.clear();
752 for (Account account : xmppConnectionService.getAccounts()) {
753 if (account.getStatus() != Account.State.DISABLED) {
754 if (Config.DOMAIN_LOCK != null) {
755 this.mActivatedAccounts.add(account.getJid().getLocalpart());
756 } else {
757 this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
758 }
759 }
760 }
761 final Intent intent = getIntent();
762 final ActionBar ab = getActionBar();
763 boolean init = intent != null && intent.getBooleanExtra("init", false);
764 boolean noConversations = xmppConnectionService.getConversations().size() == 0;
765 if ((init || noConversations) && ab != null) {
766 ab.setDisplayShowHomeEnabled(false);
767 ab.setDisplayHomeAsUpEnabled(false);
768 ab.setHomeButtonEnabled(false);
769 }
770 this.mKnownHosts = xmppConnectionService.getKnownHosts();
771 this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts();
772 if (this.mPendingInvite != null) {
773 mPendingInvite.invite();
774 this.mPendingInvite = null;
775 filter(null);
776 } else if (!handleIntent(getIntent())) {
777 if (mSearchEditText != null) {
778 filter(mSearchEditText.getText().toString());
779 } else {
780 filter(null);
781 }
782 } else {
783 filter(null);
784 }
785 setIntent(null);
786 }
787
788 protected boolean handleIntent(Intent intent) {
789 if (intent == null || intent.getAction() == null) {
790 return false;
791 }
792 switch (intent.getAction()) {
793 case Intent.ACTION_SENDTO:
794 case Intent.ACTION_VIEW:
795 Uri uri = intent.getData();
796 if (uri != null) {
797 Invite invite = new Invite(intent.getData(),false);
798 invite.account = intent.getStringExtra("account");
799 return invite.invite();
800 } else {
801 return false;
802 }
803 }
804 return false;
805 }
806
807 private boolean handleJid(Invite invite) {
808 List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(),invite.account);
809 if (invite.isAction(XmppUri.ACTION_JOIN)) {
810 Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
811 if (muc != null) {
812 switchToConversation(muc,invite.getBody(),false);
813 return true;
814 } else {
815 showJoinConferenceDialog(invite.getJid().toBareJid().toString());
816 return false;
817 }
818 } else if (contacts.size() == 0) {
819 showCreateContactDialog(invite.getJid().toString(), invite);
820 return false;
821 } else if (contacts.size() == 1) {
822 Contact contact = contacts.get(0);
823 if (!invite.isSafeSource() && invite.hasFingerprints()) {
824 displayVerificationWarningDialog(contact,invite);
825 } else {
826 if (invite.hasFingerprints()) {
827 if(xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
828 Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
829 }
830 }
831 if (invite.account != null) {
832 xmppConnectionService.getShortcutService().report(contact);
833 }
834 switchToConversation(contact, invite.getBody());
835 }
836 return true;
837 } else {
838 if (mMenuSearchView != null) {
839 mMenuSearchView.expandActionView();
840 mSearchEditText.setText("");
841 mSearchEditText.append(invite.getJid().toString());
842 filter(invite.getJid().toString());
843 } else {
844 mInitialJid = invite.getJid().toString();
845 }
846 return true;
847 }
848 }
849
850 private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
851 AlertDialog.Builder builder = new AlertDialog.Builder(this);
852 builder.setTitle(R.string.verify_omemo_keys);
853 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
854 final CheckBox isTrustedSource = (CheckBox) view.findViewById(R.id.trusted_source);
855 TextView warning = (TextView) view.findViewById(R.id.warning);
856 String jid = contact.getJid().toBareJid().toString();
857 SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source,jid,contact.getDisplayName()));
858 int start = spannable.toString().indexOf(jid);
859 if (start >= 0) {
860 spannable.setSpan(new TypefaceSpan("monospace"),start,start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
861 }
862 warning.setText(spannable);
863 builder.setView(view);
864 builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
865 if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
866 xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
867 }
868 switchToConversation(contact, invite.getBody());
869 });
870 builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
871 AlertDialog dialog = builder.create();
872 dialog.setCanceledOnTouchOutside(false);
873 dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
874 dialog.show();
875 }
876
877 protected void filter(String needle) {
878 if (xmppConnectionServiceBound) {
879 this.filterContacts(needle);
880 this.filterConferences(needle);
881 }
882 }
883
884 protected void filterContacts(String needle) {
885 this.contacts.clear();
886 for (Account account : xmppConnectionService.getAccounts()) {
887 if (account.getStatus() != Account.State.DISABLED) {
888 for (Contact contact : account.getRoster().getContacts()) {
889 Presence.Status s = contact.getShownStatus();
890 if (contact.showInRoster() && contact.match(this, needle)
891 && (!this.mHideOfflineContacts
892 || (needle != null && !needle.trim().isEmpty())
893 || s.compareTo(Presence.Status.OFFLINE) < 0)) {
894 this.contacts.add(contact);
895 }
896 }
897 }
898 }
899 Collections.sort(this.contacts);
900 mContactsAdapter.notifyDataSetChanged();
901 }
902
903 protected void filterConferences(String needle) {
904 this.conferences.clear();
905 for (Account account : xmppConnectionService.getAccounts()) {
906 if (account.getStatus() != Account.State.DISABLED) {
907 for (Bookmark bookmark : account.getBookmarks()) {
908 if (bookmark.match(this, needle)) {
909 this.conferences.add(bookmark);
910 }
911 }
912 }
913 }
914 Collections.sort(this.conferences);
915 mConferenceAdapter.notifyDataSetChanged();
916 }
917
918 private void onTabChanged() {
919 invalidateOptionsMenu();
920 }
921
922 @Override
923 public void OnUpdateBlocklist(final Status status) {
924 refreshUi();
925 }
926
927 @Override
928 protected void refreshUiReal() {
929 if (mSearchEditText != null) {
930 filter(mSearchEditText.getText().toString());
931 }
932 }
933
934 public class ListPagerAdapter extends PagerAdapter {
935 FragmentManager fragmentManager;
936 MyListFragment[] fragments;
937
938 public ListPagerAdapter(FragmentManager fm) {
939 fragmentManager = fm;
940 fragments = new MyListFragment[2];
941 }
942
943 public void requestFocus(int pos) {
944 if (fragments.length > pos) {
945 fragments[pos].getListView().requestFocus();
946 }
947 }
948
949 @Override
950 public void destroyItem(ViewGroup container, int position, Object object) {
951 assert (0 <= position && position < fragments.length);
952 FragmentTransaction trans = fragmentManager.beginTransaction();
953 trans.remove(fragments[position]);
954 trans.commit();
955 fragments[position] = null;
956 }
957
958 @Override
959 public Fragment instantiateItem(ViewGroup container, int position) {
960 Fragment fragment = getItem(position);
961 FragmentTransaction trans = fragmentManager.beginTransaction();
962 trans.add(container.getId(), fragment, "fragment:" + position);
963 trans.commit();
964 return fragment;
965 }
966
967 @Override
968 public int getCount() {
969 return fragments.length;
970 }
971
972 @Override
973 public boolean isViewFromObject(View view, Object fragment) {
974 return ((Fragment) fragment).getView() == view;
975 }
976
977 public Fragment getItem(int position) {
978 assert (0 <= position && position < fragments.length);
979 if (fragments[position] == null) {
980 final MyListFragment listFragment = new MyListFragment();
981 if (position == 1) {
982 listFragment.setListAdapter(mConferenceAdapter);
983 listFragment.setContextMenu(R.menu.conference_context);
984 listFragment.setOnListItemClickListener(new OnItemClickListener() {
985
986 @Override
987 public void onItemClick(AdapterView<?> arg0, View arg1,
988 int position, long arg3) {
989 openConversationForBookmark(position);
990 }
991 });
992 } else {
993
994 listFragment.setListAdapter(mContactsAdapter);
995 listFragment.setContextMenu(R.menu.contact_context);
996 listFragment.setOnListItemClickListener(new OnItemClickListener() {
997
998 @Override
999 public void onItemClick(AdapterView<?> arg0, View arg1,
1000 int position, long arg3) {
1001 openConversationForContact(position);
1002 }
1003 });
1004 }
1005 fragments[position] = listFragment;
1006 }
1007 return fragments[position];
1008 }
1009 }
1010
1011 public static class MyListFragment extends ListFragment {
1012 private AdapterView.OnItemClickListener mOnItemClickListener;
1013 private int mResContextMenu;
1014
1015 public void setContextMenu(final int res) {
1016 this.mResContextMenu = res;
1017 }
1018
1019 @Override
1020 public void onListItemClick(final ListView l, final View v, final int position, final long id) {
1021 if (mOnItemClickListener != null) {
1022 mOnItemClickListener.onItemClick(l, v, position, id);
1023 }
1024 }
1025
1026 public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1027 this.mOnItemClickListener = l;
1028 }
1029
1030 @Override
1031 public void onViewCreated(final View view, final Bundle savedInstanceState) {
1032 super.onViewCreated(view, savedInstanceState);
1033 registerForContextMenu(getListView());
1034 getListView().setFastScrollEnabled(true);
1035 }
1036
1037 @Override
1038 public void onCreateContextMenu(final ContextMenu menu, final View v,
1039 final ContextMenuInfo menuInfo) {
1040 super.onCreateContextMenu(menu, v, menuInfo);
1041 final StartConversationActivity activity = (StartConversationActivity) getActivity();
1042 activity.getMenuInflater().inflate(mResContextMenu, menu);
1043 final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1044 if (mResContextMenu == R.menu.conference_context) {
1045 activity.conference_context_id = acmi.position;
1046 } else if (mResContextMenu == R.menu.contact_context) {
1047 activity.contact_context_id = acmi.position;
1048 final Contact contact = (Contact) activity.contacts.get(acmi.position);
1049 final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1050 final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1051 if (contact.isSelf()) {
1052 showContactDetailsItem.setVisible(false);
1053 }
1054 XmppConnection xmpp = contact.getAccount().getXmppConnection();
1055 if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1056 if (contact.isBlocked()) {
1057 blockUnblockItem.setTitle(R.string.unblock_contact);
1058 } else {
1059 blockUnblockItem.setTitle(R.string.block_contact);
1060 }
1061 } else {
1062 blockUnblockItem.setVisible(false);
1063 }
1064 }
1065 }
1066
1067 @Override
1068 public boolean onContextItemSelected(final MenuItem item) {
1069 StartConversationActivity activity = (StartConversationActivity) getActivity();
1070 switch (item.getItemId()) {
1071 case R.id.context_start_conversation:
1072 activity.openConversationForContact();
1073 break;
1074 case R.id.context_contact_details:
1075 activity.openDetailsForContact();
1076 break;
1077 case R.id.context_contact_block_unblock:
1078 activity.toggleContactBlock();
1079 break;
1080 case R.id.context_delete_contact:
1081 activity.deleteContact();
1082 break;
1083 case R.id.context_join_conference:
1084 activity.openConversationForBookmark();
1085 break;
1086 case R.id.context_share_uri:
1087 activity.shareBookmarkUri();
1088 break;
1089 case R.id.context_delete_conference:
1090 activity.deleteConference();
1091 }
1092 return true;
1093 }
1094 }
1095
1096 private class Invite extends XmppUri {
1097
1098 public Invite(final Uri uri) {
1099 super(uri);
1100 }
1101
1102 public Invite(final String uri) {
1103 super(uri);
1104 }
1105
1106 public Invite(Uri uri, boolean safeSource) {
1107 super(uri,safeSource);
1108 }
1109
1110 public String account;
1111
1112 boolean invite() {
1113 if (!isJidValid()) {
1114 Toast.makeText(StartConversationActivity.this,R.string.invalid_jid,Toast.LENGTH_SHORT).show();
1115 return false;
1116 }
1117 if (getJid() != null) {
1118 return handleJid(this);
1119 }
1120 return false;
1121 }
1122 }
1123}