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