StartConversationActivity.java

   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.ui.service.EmojiService;
  79import eu.siacs.conversations.utils.XmppUri;
  80import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  81import eu.siacs.conversations.xmpp.XmppConnection;
  82import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  83import eu.siacs.conversations.xmpp.jid.Jid;
  84
  85public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist {
  86
  87    public int conference_context_id;
  88    public int contact_context_id;
  89    private Tab mContactsTab;
  90    private Tab mConferencesTab;
  91    private ViewPager mViewPager;
  92    private ListPagerAdapter mListPagerAdapter;
  93    private List<ListItem> contacts = new ArrayList<>();
  94    private ListItemAdapter mContactsAdapter;
  95    private List<ListItem> conferences = new ArrayList<>();
  96    private ListItemAdapter mConferenceAdapter;
  97    private List<String> mActivatedAccounts = new ArrayList<>();
  98    private List<String> mKnownHosts;
  99    private List<String> mKnownConferenceHosts;
 100    private Invite mPendingInvite = null;
 101    private EditText mSearchEditText;
 102    private AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
 103    private final int REQUEST_SYNC_CONTACTS = 0x3b28cf;
 104    private final int REQUEST_CREATE_CONFERENCE = 0x3b39da;
 105    private Dialog mCurrentDialog = null;
 106
 107    private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
 108
 109        @Override
 110        public boolean onMenuItemActionExpand(MenuItem item) {
 111            mSearchEditText.post(new Runnable() {
 112
 113                @Override
 114                public void run() {
 115                    mSearchEditText.requestFocus();
 116                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 117                    imm.showSoftInput(mSearchEditText,
 118                            InputMethodManager.SHOW_IMPLICIT);
 119                }
 120            });
 121
 122            return true;
 123        }
 124
 125        @Override
 126        public boolean onMenuItemActionCollapse(MenuItem item) {
 127            hideKeyboard();
 128            mSearchEditText.setText("");
 129            filter(null);
 130            return true;
 131        }
 132    };
 133    private boolean mHideOfflineContacts = false;
 134    private TabListener mTabListener = new TabListener() {
 135
 136        @Override
 137        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
 138            return;
 139        }
 140
 141        @Override
 142        public void onTabSelected(Tab tab, FragmentTransaction ft) {
 143            mViewPager.setCurrentItem(tab.getPosition());
 144            onTabChanged();
 145        }
 146
 147        @Override
 148        public void onTabReselected(Tab tab, FragmentTransaction ft) {
 149            return;
 150        }
 151    };
 152    private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
 153        @Override
 154        public void onPageSelected(int position) {
 155            if (getActionBar() != null) {
 156                getActionBar().setSelectedNavigationItem(position);
 157            }
 158            onTabChanged();
 159        }
 160    };
 161    private TextWatcher mSearchTextWatcher = new TextWatcher() {
 162
 163        @Override
 164        public void afterTextChanged(Editable editable) {
 165            filter(editable.toString());
 166        }
 167
 168        @Override
 169        public void beforeTextChanged(CharSequence s, int start, int count,
 170                                      int after) {
 171        }
 172
 173        @Override
 174        public void onTextChanged(CharSequence s, int start, int before, int count) {
 175        }
 176    };
 177
 178    private TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
 179        @Override
 180        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 181            int pos = getActionBar().getSelectedNavigationIndex();
 182            if (pos == 0) {
 183                if (contacts.size() == 1) {
 184                    openConversationForContact((Contact) contacts.get(0));
 185                    return true;
 186                }
 187            } else {
 188                if (conferences.size() == 1) {
 189                    openConversationsForBookmark((Bookmark) conferences.get(0));
 190                    return true;
 191                }
 192            }
 193            hideKeyboard();
 194            mListPagerAdapter.requestFocus(pos);
 195            return true;
 196        }
 197    };
 198    private MenuItem mMenuSearchView;
 199    private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
 200        @Override
 201        public void onTagClicked(String tag) {
 202            if (mMenuSearchView != null) {
 203                mMenuSearchView.expandActionView();
 204                mSearchEditText.setText("");
 205                mSearchEditText.append(tag);
 206                filter(tag);
 207            }
 208        }
 209    };
 210    private String mInitialJid;
 211    private Pair<Integer, Intent> mPostponedActivityResult;
 212    private UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
 213        @Override
 214        public void success(final Conversation conversation) {
 215            runOnUiThread(new Runnable() {
 216                @Override
 217                public void run() {
 218                    hideToast();
 219                    switchToConversation(conversation);
 220                }
 221            });
 222        }
 223
 224        @Override
 225        public void error(final int errorCode, Conversation object) {
 226            runOnUiThread(new Runnable() {
 227                @Override
 228                public void run() {
 229                    replaceToast(getString(errorCode));
 230                }
 231            });
 232        }
 233
 234        @Override
 235        public void userInputRequried(PendingIntent pi, Conversation object) {
 236
 237        }
 238    };
 239    private Toast mToast;
 240
 241    protected void hideToast() {
 242        if (mToast != null) {
 243            mToast.cancel();
 244        }
 245    }
 246
 247    protected void replaceToast(String msg) {
 248        hideToast();
 249        mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
 250        mToast.show();
 251    }
 252
 253    @Override
 254    public void onRosterUpdate() {
 255        this.refreshUi();
 256    }
 257
 258    @Override
 259    public void onCreate(Bundle savedInstanceState) {
 260        super.onCreate(savedInstanceState);
 261        new EmojiService(this).init();
 262        setContentView(R.layout.activity_start_conversation);
 263        mViewPager = findViewById(R.id.start_conversation_view_pager);
 264        ActionBar actionBar = getActionBar();
 265        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
 266
 267        mContactsTab = actionBar.newTab().setText(R.string.contacts)
 268                .setTabListener(mTabListener);
 269        mConferencesTab = actionBar.newTab().setText(R.string.conferences)
 270                .setTabListener(mTabListener);
 271        actionBar.addTab(mContactsTab);
 272        actionBar.addTab(mConferencesTab);
 273
 274        mViewPager.setOnPageChangeListener(mOnPageChangeListener);
 275        mListPagerAdapter = new ListPagerAdapter(getFragmentManager());
 276        mViewPager.setAdapter(mListPagerAdapter);
 277
 278        mConferenceAdapter = new ListItemAdapter(this, conferences);
 279        mContactsAdapter = new ListItemAdapter(this, contacts);
 280        mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
 281        this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false);
 282
 283    }
 284
 285    @Override
 286    public void onStart() {
 287        super.onStart();
 288        final int theme = findTheme();
 289        if (this.mTheme != theme) {
 290            recreate();
 291        } else {
 292            askForContactsPermissions();
 293        }
 294        mConferenceAdapter.refreshSettings();
 295        mContactsAdapter.refreshSettings();
 296    }
 297
 298    @Override
 299    public void onStop() {
 300        if (mCurrentDialog != null) {
 301            mCurrentDialog.dismiss();
 302        }
 303        super.onStop();
 304    }
 305
 306    @Override
 307    public void onNewIntent(Intent intent) {
 308        if (xmppConnectionServiceBound) {
 309            handleIntent(intent);
 310        } else {
 311            setIntent(intent);
 312        }
 313    }
 314
 315    protected void openConversationForContact(int position) {
 316        Contact contact = (Contact) contacts.get(position);
 317        openConversationForContact(contact);
 318    }
 319
 320    protected void openConversationForContact(Contact contact) {
 321        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), 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.dialog_title_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.dialog_title_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.dialog_title_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 (!isJidValid()) {
1197                Toast.makeText(StartConversationActivity.this,R.string.invalid_jid,Toast.LENGTH_SHORT).show();
1198                return false;
1199            }
1200            if (getJid() != null) {
1201                return handleJid(this);
1202            }
1203            return false;
1204        }
1205
1206        public boolean isMuc() {
1207            return muc;
1208        }
1209    }
1210}