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