StartConversationActivity.java

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